diff --git a/CREDITS.md b/CREDITS.md index a37947eb32..5023b076b7 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -704,6 +704,7 @@ This page lists all the individual contributions to the project by their author. - Global default value for `DefaultToGuardArea` - Weapon range finding in cylinder - Allow jumpjet climbing ignore building height + - Extra threat - **solar-III (凤九歌)** - Target scanning delay customization (documentation) - Skip target scanning function calling for unarmed technos (documentation) diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 647b54db92..7a3b2213cf 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1346,6 +1346,17 @@ AllowAirstrike= ; boolean AirstrikeTargets=buildings ; List of Affected Target Enumeration (none|infantry|units|buildings|all) ``` +### Allow techno type considered as other type when recruiting techno for teams + +- It is now possible to make techno type considered as other type when recruiting techno for teams, both for AI team recruitment and `Create Team` action. + - Only affect techno that's presented on the map. Cannot make AI produce this type of techno if it doesn't have any. + +In `rulesmd.ini`: +```ini +[SOMETECHNO] ; TechnoType +TeamMember.ConsideredAs= ; List of TechnoTypes +``` + ### Alternate FLH customizations - `AlternateFLH.OnTurret` can be used to customize whether or not `AlternateFLH` used for `OpenTopped` transport firing coordinates, multiple mind control link offsets etc. is calculated relative to the unit's turret if available or body. @@ -1896,17 +1907,6 @@ HeightShadowScaling.MinScale=0.0 ; floating point value ShadowSizeCharacteristicHeight= ; integer, height in leptons ``` -### Allow techno type considered as other type when recruiting techno for teams - -- It is now possible to make techno type considered as other type when recruiting techno for teams, both for AI team recruitment and `Create Team` action. - - Only affect techno that's presented on the map. Cannot make AI produce this type of techno if it doesn't have any. - -In `rulesmd.ini`: -```ini -[SOMETECHNO] ; TechnoType -TeamMember.ConsideredAs= ; List of TechnoTypes -``` - ## Terrains ### Animated TerrainTypes diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 83f2fafb5d..c153971476 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1690,6 +1690,40 @@ RateDown.Cover.Value=0 ; integer RateDown.Cover.AmmoBelow=-2 ; integer ``` +### Extra threat + +- Now you can adjust the techno's evaluation of the threat posed by the target in more ways. This will help the techno in auto - targeting. + - When the target poses a threat to the techno, it will receive an additional threat value defined by `ExtraThreat.IsThreat`. + - Generally speaking, "Posing a threat" means the target can fire at the techno. + - Using `AlwaysConsideredThreat` makes the target always considered to be a threat. + - When the target is within the techno's range, it will receive an additional threat value defined by `ExtraThreat.InRange`. + - When the target is within the techno's range, it will receive an additional threat value equal to `ExtraThreatCoefficient.InRangeDistance` multiplied by the distance (in cells) from the target to the techno. + - Only considering in-range is because the vanilla flag `TargetDistanceCoefficientDefault` only considers outside-range. This flag is complementary to that. + - The target will receive an additional threat value equal to `ExtraThreatCoefficient.Facing` multiplied by the difference in facing from the techno's current firing-facing to the target's facing. + - "Firing-facing" refers to the facing the techno uses to check if it "is already facing the target and can fire". Infantry doesn't check the facing when firing, so this is also invalid for infantry. + - The unit of facing is the in-game internal numerical scale. A full circle corresponds to 65536. + - The target will receive an additional threat value equal to `ExtraThreatCoefficient.DistanceToLastTarget` multiplied by the distance (in cells) from the target to the techno's last target. + - Each techno will record its current target as the "last target" per frame. This record will be retained for at most 15 frames after the target becomes invalid. + - If the techno doesn't have a "last target", then this will not take effect. + +In `rulesmd.ini`: +```ini +[General] +ExtraThreat.IsThreat=0.0 ; double +ExtraThreat.InRange=0.0 ; double +ExtraThreatCoefficient.InRangeDistance=0.0 ; double +ExtraThreatCoefficient.Facing=0.0 ; double +ExtraThreatCoefficient.DistanceToLastTarget=0.0 ; double + +[SOMETECHNO] ; TechnoType +AlwaysConsideredThreat=false +ExtraThreat.IsThreat= ; double, default to the flag in [General] with same name +ExtraThreat.InRange= ; double, default to the flag in [General] with same name +ExtraThreatCoefficient.InRangeDistance= ; double, default to the flag in [General] with same name +ExtraThreatCoefficient.Facing= ; double, default to the flag in [General] with same name +ExtraThreatCoefficient.DistanceToLastTarget= ; double, default to the flag in [General] with same name +``` + ### Firing offsets for specific Burst shots - You can now specify separate firing offsets for each of the shots fired by weapon with `Burst` via using `(Elite)(Prone/Deployed)PrimaryFire|SecondaryFire|WeaponX|FLH.BurstN` keys, depending on which weapons your TechnoType makes use of. *N* in `BurstN` is zero-based burst shot index, and the values are parsed sequentially until no value for either regular or elite weapon is present, with elite weapon defaulting to regular weapon FLH if only it is missing. If no burst-index specific value is available, value from the base key (f.ex `PrimaryFireFLH`) is used. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index cf01c81975..d38cf687ff 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -602,6 +602,7 @@ Vanilla fixes: - Fixed the bug in AI scripts 56 and 57 that forced the launch of superweapons with index numbers 3 and 4 (by FlyStar) - Buildings with `NeedsEngineer=true` are now considered to have threat value of 0 under ownership of `MultiplayPassive=true` houses regardless of their `ThreatPosed` value (by Starkku) - Vehicles overlapping `Wall=true` OverlayTypes no longer display sell cursor and cannot be sold (by CnCRAZER & Starkku) +- Extra threat (by TaranDahl) Phobos fixes: - Fixed the bug that `AllowAirstrike=no` cannot completely prevent air strikes from being launched against it (by NetsuNegi) diff --git a/src/Ext/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 228b0415ca..dc1eb06b99 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -370,6 +370,12 @@ void RulesExt::ExtData::LoadBeforeTypeData(RulesClass* pThis, CCINIClass* pINI) this->UnitsUnsellable.Read(exINI, GameStrings::General, "UnitsUnsellable"); + this->ExtraThreat_IsThreat.Read(exINI, GameStrings::General, "ExtraThreat.IsThreat"); + this->ExtraThreat_InRange.Read(exINI, GameStrings::General, "ExtraThreat.InRange"); + this->ExtraThreatCoefficient_InRangeDistance.Read(exINI, GameStrings::General, "ExtraThreatCoefficient.InRangeDistance"); + this->ExtraThreatCoefficient_Facing.Read(exINI, GameStrings::General, "ExtraThreatCoefficient.Facing"); + this->ExtraThreatCoefficient_DistanceToLastTarget.Read(exINI, GameStrings::General, "ExtraThreatCoefficient.DistanceToLastTarget"); + // Section AITargetTypes int itemsCount = pINI->GetKeyCount("AITargetTypes"); for (int i = 0; i < itemsCount; ++i) @@ -673,6 +679,11 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->CylinderRangefinding) .Process(this->PenetratesTransport_Level) .Process(this->UnitsUnsellable) + .Process(this->ExtraThreat_IsThreat) + .Process(this->ExtraThreat_InRange) + .Process(this->ExtraThreatCoefficient_InRangeDistance) + .Process(this->ExtraThreatCoefficient_Facing) + .Process(this->ExtraThreatCoefficient_DistanceToLastTarget) ; } diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index ca624cd9ae..18cf9a6b9f 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -320,6 +320,12 @@ class RulesExt Valueable UnitsUnsellable; + Valueable ExtraThreat_IsThreat; + Valueable ExtraThreat_InRange; + Valueable ExtraThreatCoefficient_InRangeDistance; + Valueable ExtraThreatCoefficient_Facing; + Valueable ExtraThreatCoefficient_DistanceToLastTarget; + ExtData(RulesClass* OwnerObject) : Extension(OwnerObject) , Storage_TiberiumIndex { -1 } , HarvesterDumpAmount { 0.0f } @@ -583,6 +589,12 @@ class RulesExt , PenetratesTransport_Level { 10 } , UnitsUnsellable { false } + + , ExtraThreat_IsThreat { 0.0 } + , ExtraThreat_InRange { 0.0 } + , ExtraThreatCoefficient_InRangeDistance { 0.0 } + , ExtraThreatCoefficient_Facing { 0.0 } + , ExtraThreatCoefficient_DistanceToLastTarget { 0.0 } { } virtual ~ExtData() = default; diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 11550f9c55..8a226f2173 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -39,6 +39,8 @@ void TechnoExt::ExtData::OnEarlyUpdate() if (this->AttackMoveFollowerTempCount) this->AttackMoveFollowerTempCount--; + + this->UpdateLastTargetCrd(); } void TechnoExt::ExtData::ApplyInterceptor() @@ -2169,3 +2171,29 @@ void TechnoExt::ExtData::UpdateTintValues() calculateTint(Drawing::RGB_To_Int(pShieldType->Tint_Color), static_cast(pShieldType->Tint_Intensity * 1000), pShieldType->Tint_VisibleToHouses); } } + +void TechnoExt::ExtData::UpdateLastTargetCrd() +{ + if (!this->TypeExtData->ExtraThreat_Enabled) + return; + + auto const pThis = this->OwnerObject(); + auto pTimer = &this->LastTargetCrdClearTimer; + + if (pThis->Target) + { + this->LastTargetCrd = pThis->Target->GetCoords(); + pTimer->Stop(); + } + else + { + if (!pTimer->IsTicking()) + pTimer->Start(15); + + if (pTimer->Completed()) + { + this->LastTargetCrd = CoordStruct::Empty; + pTimer->Stop(); + } + } +} diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index ed4ebd3ce3..7f7dfba0a7 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -1063,6 +1063,8 @@ void TechnoExt::ExtData::Serialize(T& Stm) .Process(this->SpecialTracked) .Process(this->FallingDownTracked) .Process(this->JumpjetStraightAscend) + .Process(this->LastTargetCrd) + .Process(this->LastTargetCrdClearTimer) ; } diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 7f178426b4..79e517618e 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -100,6 +100,9 @@ class TechnoExt bool JumpjetStraightAscend; // Is set to true jumpjet units will ascend straight and do not adjust rotation or position during it. + CoordStruct LastTargetCrd; + CDTimerClass LastTargetCrdClearTimer; + ExtData(TechnoClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } , Shield {} @@ -166,6 +169,8 @@ class TechnoExt , SpecialTracked { false } , FallingDownTracked { false } , JumpjetStraightAscend { false } + , LastTargetCrd { CoordStruct::Empty } + , LastTargetCrdClearTimer {} { } void OnEarlyUpdate(); @@ -206,6 +211,7 @@ class TechnoExt void UpdateTintValues(); void AmmoAutoConvertActions(); + void UpdateLastTargetCrd(); virtual ~ExtData() override; virtual void InvalidatePointer(void* ptr, bool bRemoved) override; diff --git a/src/Ext/Techno/Hooks.TargetEvaluation.cpp b/src/Ext/Techno/Hooks.TargetEvaluation.cpp index 67fde96409..7e551ecaaf 100644 --- a/src/Ext/Techno/Hooks.TargetEvaluation.cpp +++ b/src/Ext/Techno/Hooks.TargetEvaluation.cpp @@ -377,4 +377,137 @@ static Action __fastcall InfantryClass__WhatAction_Wrapper(InfantryClass* pThis, } DEFINE_FUNCTION_JUMP(VTABLE, 0x7EB0CC, InfantryClass__WhatAction_Wrapper) +#pragma endregion + +#pragma region ThreatEvaluation + +// Current target may hurt me. +static inline bool IsAThreatToMe(TechnoClass* const pTechno, AbstractClass* const pTarget, int weaponIndex = -1) +{ + if (const auto pTechnoTarget = abstract_cast(pTarget)) + { + auto pTypeExt = TechnoExt::ExtMap.Find(pTechnoTarget)->TypeExtData; + + if (pTypeExt->AlwaysConsideredThreat) + return true; + + if (weaponIndex < 0) + weaponIndex = pTechnoTarget->SelectWeapon(pTechno); + + if (!pTechnoTarget->GetWeapon(weaponIndex)->WeaponType) + return false; + + const auto error = pTechnoTarget->GetFireError(pTechno, weaponIndex, true); + return pTechnoTarget->WhatAmI() == AbstractType::Building ? (error != FireError::ILLEGAL) && (error != FireError::RANGE) : (error != FireError::ILLEGAL); + } + + return false; +} + +// Decide the facing to check for firing. +static inline FacingClass* GetFireFacing(TechnoClass* const pTechno) +{ + if (!pTechno) + return nullptr; + + switch (pTechno->WhatAmI()) + { + case AbstractType::Building: + return &pTechno->PrimaryFacing; + case AbstractType::Unit: + { + if (pTechno->GetTechnoType()->Turret) + return &pTechno->SecondaryFacing; + else + return &pTechno->PrimaryFacing; + } + case AbstractType::Infantry: + return nullptr; + case AbstractType::Aircraft: + return &pTechno->SecondaryFacing; + default: + return nullptr; + } +} + +DEFINE_HOOK(0x70CF87, TechnoClass_ThreatCoefficient_CanAttackMeThreatBonus, 0x9) +{ + GET(TechnoClass* const, pThis, EDI); + GET(TechnoClass* const, pTarget, ESI); + REF_STACK(double, totalThreat, STACK_OFFSET(0x58, -0x48)); + + auto pExt = TechnoExt::ExtMap.Find(pThis); + auto pTypeExt = pExt->TypeExtData; + + if (!pTypeExt->ExtraThreat_Enabled) + return 0; + + auto ApplyIsThreatBonus = [pTypeExt, pThis, pTarget, &totalThreat]() + { + double bonus = pTypeExt->ExtraThreat_IsThreat.Get(RulesExt::Global()->ExtraThreat_IsThreat); + + if (bonus == 0.0) + return; + + if (!IsAThreatToMe(pThis, pTarget)) + return; + + totalThreat += bonus; + }; + ApplyIsThreatBonus(); + + auto ApplyInRangeBonus = [pTypeExt, pThis, pTarget, &totalThreat]() + { + double bonus1 = pTypeExt->ExtraThreat_InRange.Get(RulesExt::Global()->ExtraThreat_InRange); + double dist = pThis->DistanceFrom(pTarget) / 256.0; + double bonus2 = dist * pTypeExt->ExtraThreatCoefficient_InRangeDistance.Get(RulesExt::Global()->ExtraThreatCoefficient_InRangeDistance); + double bonus = bonus1 + bonus2; + + if (bonus == 0.0) + return; + + if (!pThis->IsCloseEnoughToAttack(pTarget)) + return; + + totalThreat += bonus; + }; + ApplyInRangeBonus(); + + auto ApplyFacingBonus = [pTypeExt, pThis, pTarget, &totalThreat]() + { + auto pFacing = GetFireFacing(pThis); + + if (!pFacing) + return; + + double bonus = pTypeExt->ExtraThreatCoefficient_Facing.Get(RulesExt::Global()->ExtraThreatCoefficient_Facing); + + if (bonus == 0.0) + return; + + DirStruct dir = DirStruct(); + int deltaFacing = std::abs(pThis->GetTargetDirection(&dir, pTarget)->Raw - pFacing->Current().Raw); + totalThreat += deltaFacing * bonus; + }; + ApplyFacingBonus(); + + auto ApplyLastTargetDistanceBonus = [pExt, pTypeExt, pThis, pTarget, &totalThreat]() + { + double bonus = pTypeExt->ExtraThreatCoefficient_DistanceToLastTarget.Get(RulesExt::Global()->ExtraThreatCoefficient_DistanceToLastTarget); + + if (bonus == 0.0) + return; + + if (pExt->LastTargetCrd == CoordStruct::Empty) + return; + + double distToLastTarget = pTarget->GetCoords().DistanceFrom(pExt->LastTargetCrd) / 256.0; + totalThreat += distToLastTarget * bonus; + }; + ApplyLastTargetDistanceBonus(); + + return 0; +} + + #pragma endregion diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index 8cc82868c4..dc416e4947 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -1160,6 +1160,18 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->JumpjetClimbIgnoreBuilding.Read(exINI, pSection, "JumpjetClimbIgnoreBuilding"); + this->ExtraThreat_IsThreat.Read(exINI, pSection, "ExtraThreat.IsThreat"); + this->AlwaysConsideredThreat.Read(exINI, pSection, "AlwaysConsideredThreat"); + this->ExtraThreat_InRange.Read(exINI, pSection, "ExtraThreat.InRange"); + this->ExtraThreatCoefficient_InRangeDistance.Read(exINI, pSection, "ExtraThreatCoefficient.InRangeDistance"); + this->ExtraThreatCoefficient_Facing.Read(exINI, pSection, "ExtraThreatCoefficient.Facing"); + this->ExtraThreatCoefficient_DistanceToLastTarget.Read(exINI, pSection, "ExtraThreatCoefficient.DistanceToLastTarget"); + this->ExtraThreat_Enabled = ExtraThreat_IsThreat.Get(RulesExt::Global()->ExtraThreat_IsThreat) != 0 + || !ExtraThreat_InRange.Get(RulesExt::Global()->ExtraThreat_InRange) != 0 + || ExtraThreatCoefficient_InRangeDistance.Get(RulesExt::Global()->ExtraThreatCoefficient_InRangeDistance) != 0 + || ExtraThreatCoefficient_Facing.Get(RulesExt::Global()->ExtraThreatCoefficient_Facing) != 0 + || ExtraThreatCoefficient_DistanceToLastTarget.Get(RulesExt::Global()->ExtraThreatCoefficient_DistanceToLastTarget) != 0; + // Ares 0.2 this->RadarJamRadius.Read(exINI, pSection, "RadarJamRadius"); @@ -1876,6 +1888,14 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->JumpjetClimbIgnoreBuilding) .Process(this->Unsellable) + + .Process(this->ExtraThreat_Enabled) + .Process(this->ExtraThreat_IsThreat) + .Process(this->AlwaysConsideredThreat) + .Process(this->ExtraThreat_InRange) + .Process(this->ExtraThreatCoefficient_InRangeDistance) + .Process(this->ExtraThreatCoefficient_Facing) + .Process(this->ExtraThreatCoefficient_DistanceToLastTarget) ; } void TechnoTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index c11729ec83..47f28530d6 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -485,6 +485,14 @@ class TechnoTypeExt Nullable JumpjetClimbIgnoreBuilding; + bool ExtraThreat_Enabled; + Nullable ExtraThreat_IsThreat; + Valueable AlwaysConsideredThreat; + Nullable ExtraThreat_InRange; + Nullable ExtraThreatCoefficient_InRangeDistance; + Nullable ExtraThreatCoefficient_Facing; + Nullable ExtraThreatCoefficient_DistanceToLastTarget; + Nullable Unsellable; // Ares 3.0 ExtData(TechnoTypeClass* OwnerObject) : Extension(OwnerObject) @@ -927,6 +935,14 @@ class TechnoTypeExt , JumpjetClimbIgnoreBuilding {} , Unsellable {} + + , ExtraThreat_Enabled { false } + , ExtraThreat_IsThreat {} + , AlwaysConsideredThreat { false } + , ExtraThreat_InRange {} + , ExtraThreatCoefficient_InRangeDistance {} + , ExtraThreatCoefficient_Facing {} + , ExtraThreatCoefficient_DistanceToLastTarget {} { } virtual ~ExtData() = default;