diff --git a/CREDITS.md b/CREDITS.md index bdb498d967..afb529a0f6 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -530,6 +530,8 @@ This page lists all the individual contributions to the project by their author. - Burst without delay - Fix an issue that if the garrison unload occupants when there is no open space around it would result in the disappearance of the occupants - Fix an issue where Ares' `Convert.Deploy` triggers repeatedly when the unit is turning or moving + - Customized Vehicle Turret Rotation + - Extra threat value coefficient related to orientation - **Ollerus**: - Build limit group enhancement - Customizable rocker amplitude diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 2509d78ba7..86dc0026a7 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -154,6 +154,7 @@ + diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 0ed79088c9..d9c6942402 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1536,6 +1536,22 @@ RateDown.Cover.Value=0 ; integer RateDown.Cover.AmmoBelow=-2 ; integer ``` +### Extra threat value coefficient related to orientation + +- Now, you can enable extra threat value coefficients related to the target direction through `TargetExtraThreat`. + - `TargetExtraThreat.Angles` needs to be filled in from small to large. When the angle between the orientation and the target direction starts to be smaller than one of the values, the threat coefficient will be multiplied by the corresponding value in `TargetExtraThreat.Multipliers`. When the ultimate threat to the target is 0, the techno will not actively attack the target. + - `TargetExtraThreat.Multipliers` must be used in conjunction with `TargetExtraThreat.Angles` and can have an additional factor representing the threat value coefficient when the maximum angle is exceeded. + - `TargetExtraThreat.Turret` controls whether the orientation of the turret will be prioritized when calculating the orientation of the techno. If you want to use it in conjunction with `Turret.Restriction`, setting it to false would have the best effect. + +In `rulesmd.ini`: +```ini +[SOMETECHNO] ; TechnoType +TargetExtraThreat=false ; boolean +TargetExtraThreat.Angles= ; List of floating point value +TargetExtraThreat.Multipliers= ; List of floating point value +TargetExtraThreat.Turret=true ; boolean +``` + ### 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. @@ -2006,6 +2022,48 @@ AmphibiousEnter= ; boolean, default to [General] -> AmphibiousEnter AmphibiousUnload= ; boolean, default to [General] -> AmphibiousUnload ``` +### Customized Vehicle Turret Rotation + +- When `ExpandTurretRotation` is set to true, the following functions will be enabled. +- Units with turret without `TurretSpins=true` can looks more vivid when it is in idle. + - `Turret.IdleRotate` controls whether units can rotate their turrets when in idle. + - `Turret.PointToMouse` controls whether units will turn their turrets to your mouse when in idle. + - At the present moment, this only functions in singleplayer. + - `Turret.IdleRestartMin` and `Turret.IdleRestartMax` control the delay from idle to action occurrence together. + - `Turret.IdleIntervalMin` and `Turret.IdleIntervalMax` control the delay between every idle actions together. +- The turret and body of the units can now be rotated under control. + - `Turret.Restriction` defines the angle at which the turret can be turned to both sides. + - `Turret.ExtraAngle` defines the additional rotation angle of the turret, and `Turret.Restriction` will also rotate with this value. The positive number is clockwise and the negative number is counterclockwise, that is, what angle the turret should use by default or face the target. + - `Turret.BodyFoundation` controls whether the unit's turret will rotate in the direction of body rotation. + - `Turret.BodyOrientation` controls whether the body needs to aim at the target when firing. + - `Turret.BodyOrientationAngle` defines the additional rotation angle of the body when aiming at the target. The positive number is clockwise and the negative number is counterclockwise, that is, what angle the body should use to face the target. + - `Turret.BodyOrientationSymmetric` controls whether both sides of the additional body rotation angle can be used. + +In `rulesmd.ini`: +```ini +[General] +ExpandTurretRotation=false ; boolean + +[AudioVisual] +Turret.IdleRotate=false ; boolean +Turret.PointToMouse=false ; boolean +Turret.BodyFoundation=false ; boolean +Turret.IdleRestartMin=150 ; integer, number of frames +Turret.IdleRestartMax=300 ; integer, number of frames +Turret.IdleIntervalMin=150 ; integer, number of frames +Turret.IdleIntervalMax=450 ; integer, number of frames + +[SOMEVEHICLE] ; VehicleType, with `Turret=yes` +Turret.IdleRotate= ; boolean, default to [AudioVisual] -> Turret.IdleRotate +Turret.PointToMouse= ; boolean, default to [AudioVisual] -> Turret.PointToMouse +Turret.Restriction=180.0 ; floating point value +Turret.ExtraAngle=0 ; floating point value +Turret.BodyFoundation= ; boolean, default to [AudioVisual] -> Turret.BodyFoundation +Turret.BodyOrientation=false ; boolean +Turret.BodyOrientationAngle=0 ; floating point value +Turret.BodyOrientationSymmetric=true ; boolean +``` + ### Damaged unit image changes - When a unit is damaged (health points percentage is lower than `[AudioVisual] -> ConditionYellow` percentage), it now may use different image set by `Image.ConditionYellow` VehicleType. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index e6dee4bdbe..26783d85b1 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -428,6 +428,8 @@ New: - [Units can customize the attack voice that plays when using more weapons](New-or-Enhanced-Logics.md#multi-voiceattack) (by FlyStar) - Customize squid grapple animation (by NetsuNegi) - [Auto deploy for GI-like infantry](Fixed-or-Improved-Logics.md#auto-deploy-for-gi-like-infantry) (by TaranDahl) +- Customized Vehicle Turret Rotation (by CrimRecya) +- Extra threat value coefficient related to orientation (by CrimRecya) 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/Rules/Body.cpp b/src/Ext/Rules/Body.cpp index 6ce4e6d426..ccd277fae5 100644 --- a/src/Ext/Rules/Body.cpp +++ b/src/Ext/Rules/Body.cpp @@ -221,6 +221,15 @@ 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->ExpandTurretRotation.Read(exINI, GameStrings::General, "ExpandTurretRotation"); + this->Turret_IdleRotate.Read(exINI, GameStrings::AudioVisual, "Turret.IdleRotate"); + this->Turret_PointToMouse.Read(exINI, GameStrings::AudioVisual, "Turret.PointToMouse"); + this->Turret_BodyFoundation.Read(exINI, GameStrings::AudioVisual, "Turret.BodyFoundation"); + this->Turret_IdleRestartMin.Read(exINI, GameStrings::AudioVisual, "Turret.IdleRestartMin"); + this->Turret_IdleRestartMax.Read(exINI, GameStrings::AudioVisual, "Turret.IdleRestartMax"); + this->Turret_IdleIntervalMin.Read(exINI, GameStrings::AudioVisual, "Turret.IdleIntervalMin"); + this->Turret_IdleIntervalMax.Read(exINI, GameStrings::AudioVisual, "Turret.IdleIntervalMax"); + this->DefaultInfantrySelectBox.Read(exINI, GameStrings::AudioVisual, "DefaultInfantrySelectBox"); this->DefaultUnitSelectBox.Read(exINI, GameStrings::AudioVisual, "DefaultUnitSelectBox"); @@ -486,6 +495,14 @@ void RulesExt::ExtData::Serialize(T& Stm) .Process(this->Infantry_DefaultDigitalDisplayTypes) .Process(this->Vehicles_DefaultDigitalDisplayTypes) .Process(this->Aircraft_DefaultDigitalDisplayTypes) + .Process(this->ExpandTurretRotation) + .Process(this->Turret_IdleRotate) + .Process(this->Turret_PointToMouse) + .Process(this->Turret_BodyFoundation) + .Process(this->Turret_IdleRestartMin) + .Process(this->Turret_IdleRestartMax) + .Process(this->Turret_IdleIntervalMin) + .Process(this->Turret_IdleIntervalMax) .Process(this->DefaultInfantrySelectBox) .Process(this->DefaultUnitSelectBox) .Process(this->VisualScatter_Min) diff --git a/src/Ext/Rules/Body.h b/src/Ext/Rules/Body.h index bef203036e..bc926bd8dd 100644 --- a/src/Ext/Rules/Body.h +++ b/src/Ext/Rules/Body.h @@ -152,6 +152,15 @@ class RulesExt ValueableVector Vehicles_DefaultDigitalDisplayTypes; ValueableVector Aircraft_DefaultDigitalDisplayTypes; + Valueable ExpandTurretRotation; + Valueable Turret_IdleRotate; + Valueable Turret_PointToMouse; + Valueable Turret_BodyFoundation; + Valueable Turret_IdleRestartMin; + Valueable Turret_IdleRestartMax; + Valueable Turret_IdleIntervalMin; + Valueable Turret_IdleIntervalMax; + Valueable DefaultInfantrySelectBox; Valueable DefaultUnitSelectBox; @@ -374,6 +383,14 @@ class RulesExt , Infantry_DefaultDigitalDisplayTypes {} , Vehicles_DefaultDigitalDisplayTypes {} , Aircraft_DefaultDigitalDisplayTypes {} + , ExpandTurretRotation { false } + , Turret_IdleRotate { false } + , Turret_PointToMouse { false } + , Turret_BodyFoundation { false } + , Turret_IdleRestartMin { 150 } + , Turret_IdleRestartMax { 300 } + , Turret_IdleIntervalMin { 150 } + , Turret_IdleIntervalMax { 450 } , DefaultInfantrySelectBox {} , DefaultUnitSelectBox {} , VisualScatter_Min { Leptons(8) } diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 8d2d91efb1..828eee12f8 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -19,6 +19,8 @@ #include #include +#include +#include // TechnoClass_AI_0x6F9E50 // It's not recommended to do anything more here it could have a better place for performance consideration @@ -1298,6 +1300,135 @@ void TechnoExt::ExtData::UpdateMindControlAnim() } } +void TechnoExt::ExtData::StopIdleAction() +{ + if (this->UnitIdleActionTimer.IsTicking()) + this->UnitIdleActionTimer.Stop(); + + if (this->UnitIdleActionGapTimer.IsTicking()) + { + this->UnitIdleActionGapTimer.Stop(); + const auto pTypeExt = this->TypeExtData; + this->StopRotateWithNewROT(pTypeExt->TurretROT.Get(pTypeExt->OwnerObject()->ROT)); + } +} + +void TechnoExt::ExtData::ApplyIdleAction() +{ + const auto pThis = this->OwnerObject(); + auto shouldNotTurn = [pThis]() -> bool + { + if (const auto pUnit = abstract_cast(pThis)) + return pUnit->BunkerLinkedItem || !pUnit->Type->Speed || (pUnit->Type->IsSimpleDeployer && pUnit->Deployed); + + return pThis->WhatAmI() == AbstractType::Building; + }; + + if (this->UnitIdleActionTimer.Completed()) // Set first direction + { + this->UnitIdleActionTimer.Stop(); + this->UnitIdleActionGapTimer.Start(ScenarioClass::Instance->Random.RandomRanged(RulesExt::Global()->Turret_IdleIntervalMin, RulesExt::Global()->Turret_IdleIntervalMax)); + const short raw = static_cast(ScenarioClass::Instance->Random.RandomRanged(0, 65535) - 32768); + this->StopRotateWithNewROT(ScenarioClass::Instance->Random.RandomRanged(2,4) >> 1); + this->SetTurretDir(DirStruct { (this->TypeExtData->GetTurretLimitedRaw(shouldNotTurn() ? raw : (raw / 4)) + static_cast(pThis->PrimaryFacing.Current().Raw)) }); + return; + } + else if (this->UnitIdleActionGapTimer.IsTicking()) // Check change direction + { + if (!this->UnitIdleActionGapTimer.HasTimeLeft()) // Set next direction + { + this->UnitIdleActionGapTimer.Start(ScenarioClass::Instance->Random.RandomRanged(RulesExt::Global()->Turret_IdleIntervalMin, RulesExt::Global()->Turret_IdleIntervalMax)); + const short raw = static_cast(ScenarioClass::Instance->Random.RandomRanged(0, 65535) - 32768); + this->StopRotateWithNewROT(ScenarioClass::Instance->Random.RandomRanged(2,4) >> 1); + this->SetTurretDir(DirStruct { (this->TypeExtData->GetTurretLimitedRaw(shouldNotTurn() ? raw : (raw / 4)) + static_cast(pThis->PrimaryFacing.Current().Raw)) }); + return; + } + } + else if (!this->UnitIdleActionTimer.IsTicking()) // In idle now + { + this->UnitIdleActionTimer.Start(ScenarioClass::Instance->Random.RandomRanged(RulesExt::Global()->Turret_IdleRestartMin, RulesExt::Global()->Turret_IdleRestartMax)); + + if (!shouldNotTurn()) + { + this->SetTurretDir(pThis->PrimaryFacing.Current()); + return; + } + } + + this->UpdateIdleDir(); +} + +void TechnoExt::ExtData::ManualIdleAction() +{ + const auto pThis = this->OwnerObject(); + + if (pThis->IsSelected) + { + this->CheckIdleAction(); + this->UnitIdleIsSelected = true; + const auto mouseCoords = TacticalClass::Instance->ClientToCoords(WWMouseClass::Instance->XY1); + + if (mouseCoords != CoordStruct::Empty) // Mouse in tactical + { + const auto offset = -static_cast(pThis->GetCoords().Z * ((Unsorted::LeptonsPerCell / 2.0) / Unsorted::LevelHeight)); + const auto targetDir = pThis->GetTargetDirection(MapClass::Instance.GetCellAt(CoordStruct { mouseCoords.X - offset, mouseCoords.Y - offset, 0 })); + this->SetTurretDir(targetDir, true); + } + } + else if (this->UnitIdleIsSelected) // Immediately stop when is not selected + { + this->UnitIdleIsSelected = false; + this->StopRotateWithNewROT(); + } +} + +void TechnoExt::ExtData::CheckIdleAction() +{ + if (this->TypeExtData->Turret_IdleRotate.Get(RulesExt::Global()->Turret_IdleRotate)) + this->StopIdleAction(); +} + +void TechnoExt::ExtData::UpdateIdleDir() +{ + if (const auto pUnit = abstract_cast(this->OwnerObject())) + { + const auto pTypeExt = this->TypeExtData; + + if (static_cast(pTypeExt->Turret_Restriction.Get().Raw) < 32768) + { + const auto rotate = pTypeExt->Turret_ExtraAngle.Get(); + const auto dir = pUnit->SecondaryFacing.Desired(); + pTypeExt->SetTurretLimitedDir(pUnit, DirStruct { static_cast(pUnit, dir.Raw) - static_cast(rotate.Raw) }); + } + } +} + +void TechnoExt::ExtData::SetTurretDir(DirStruct desiredDir, bool limited) +{ + const auto pThis = this->OwnerObject(); + const auto pUnit = abstract_cast(pThis); + + if (!pUnit) + pThis->PrimaryFacing.SetDesired(desiredDir); + else if (limited) + this->TypeExtData->SetTurretLimitedDir(pUnit, desiredDir); + else + pThis->SecondaryFacing.SetDesired(this->TypeExtData->GetTurretDesiredDir(desiredDir)); +} + +void TechnoExt::ExtData::StopRotateWithNewROT(int ROT) +{ + const auto turret = &this->OwnerObject()->SecondaryFacing; + + const auto currentFacingDirection = turret->Current(); + turret->DesiredFacing = currentFacingDirection; + turret->StartFacing = currentFacingDirection; + turret->RotationTimer.Start(0); + + if (ROT >= 0) + turret->SetROT(ROT); +} + void TechnoExt::ExtData::UpdateRecountBurst() { const auto pThis = this->OwnerObject(); diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 7521101d2f..a40e9c292f 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -765,6 +765,9 @@ void TechnoExt::ExtData::Serialize(T& Stm) .Process(this->LastRearmWasFullDelay) .Process(this->CanCloakDuringRearm) .Process(this->WHAnimRemainingCreationInterval) + .Process(this->UnitIdleIsSelected) + .Process(this->UnitIdleActionTimer) + .Process(this->UnitIdleActionGapTimer) .Process(this->LastWeaponType) .Process(this->FiringObstacleCell) .Process(this->IsDetachingForCloak) diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 6493623e47..61796dab0d 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -52,6 +52,9 @@ class TechnoExt bool LastRearmWasFullDelay; bool CanCloakDuringRearm; // Current rearm timer was started by DecloakToFire=no weapon. int WHAnimRemainingCreationInterval; + bool UnitIdleIsSelected; + CDTimerClass UnitIdleActionTimer; + CDTimerClass UnitIdleActionGapTimer; WeaponTypeClass* LastWeaponType; CellClass* FiringObstacleCell; // Set on firing if there is an obstacle cell between target and techno, used for updating WaveClass target etc. bool IsDetachingForCloak; // Used for checking animation detaching, set to true before calling Detach_All() on techno when this anim is attached to and to false after when cloaking only. @@ -118,6 +121,9 @@ class TechnoExt , LastRearmWasFullDelay { false } , CanCloakDuringRearm { false } , WHAnimRemainingCreationInterval { 0 } + , UnitIdleIsSelected { false } + , UnitIdleActionTimer {} + , UnitIdleActionGapTimer {} , LastWeaponType {} , FiringObstacleCell {} , IsDetachingForCloak { false } @@ -178,6 +184,13 @@ class TechnoExt void UpdateSelfOwnedAttachEffects(); bool HasAttachedEffects(std::vector attachEffectTypes, bool requireAll, bool ignoreSameSource, TechnoClass* pInvoker, AbstractClass* pSource, std::vector const* minCounts, std::vector const* maxCounts) const; int GetAttachedEffectCumulativeCount(AttachEffectTypeClass* pAttachEffectType, bool ignoreSameSource = false, TechnoClass* pInvoker = nullptr, AbstractClass* pSource = nullptr) const; + void StopIdleAction(); + void ApplyIdleAction(); + void ManualIdleAction(); + void CheckIdleAction(); + void UpdateIdleDir(); + void SetTurretDir(DirStruct desiredDir, bool limited = false); + void StopRotateWithNewROT(int ROT = -1); void InitializeDisplayInfo(); void ApplyMindControlRangeLimit(); int ApplyForceWeaponInRange(AbstractClass* pTarget); diff --git a/src/Ext/Techno/Hooks.Facing.cpp b/src/Ext/Techno/Hooks.Facing.cpp new file mode 100644 index 0000000000..64fb1978da --- /dev/null +++ b/src/Ext/Techno/Hooks.Facing.cpp @@ -0,0 +1,173 @@ +#include "Body.h" + +#include +#include + +#include + +#pragma region UnitsFacing + +// Would it be better to rewrite the entire UpdateRotation() ? +DEFINE_HOOK(0x7369D6, UnitClass_UpdateRotation_StopUnitIdleAction, 0xA) +{ + enum { SkipGameCode = 0x736A8E }; + + GET(UnitClass* const, pThis, ESI); + + if (!RulesExt::Global()->ExpandTurretRotation) + return 0; + + if (const auto pWeaponStruct = pThis->GetTurretWeapon()) + { + const auto pWeapon = pWeaponStruct->WeaponType; + const auto pWeaponTypeExt = WeaponTypeExt::ExtMap.Find(pWeapon); + + if (pWeapon && (!pWeapon->OmniFire || (pWeaponTypeExt && pWeaponTypeExt->OmniFire_TurnToTarget))) + { + const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pThis->Type); + + if (pWeaponStruct->TurretLocked) + { + pTypeExt->SetTurretLimitedDir(pThis, pThis->PrimaryFacing.Current()); + } + else + { + const auto targetDir = pThis->GetTargetDirection(pThis->Target); + + if (pTypeExt->Turret_BodyOrientation && !pThis->Destination && !pThis->Locomotor->Is_Moving()) + { + const auto curDir = pThis->PrimaryFacing.Current(); + const auto tgtDir = pTypeExt->GetBodyDesiredDir(curDir, targetDir); + + if (std::abs(static_cast(static_cast(tgtDir.Raw) - static_cast(curDir.Raw))) >= 8192) + pThis->PrimaryFacing.SetDesired(tgtDir); + } + + pTypeExt->SetTurretLimitedDir(pThis, targetDir); + } + } + } + + return SkipGameCode; +} + +DEFINE_HOOK(0x736AEA, UnitClass_UpdateRotation_ApplyUnitIdleAction, 0x6) +{ + enum { SkipGameCode = 0x736BE2 }; + + GET(UnitClass* const, pThis, ESI); + + if (!RulesExt::Global()->ExpandTurretRotation) + return 0; + + const auto pExt = TechnoExt::ExtMap.Find(pThis); + + // Turning to target? + if (pThis->SecondaryFacing.IsRotating()) + { + // Repeatedly check TurretSpins and IsRotating() seems unnecessary + pThis->unknown_bool_6AF = true; + + if (!pExt->UnitIdleActionTimer.IsTicking() && !pExt->UnitIdleActionGapTimer.IsTicking() && !pExt->UnitIdleIsSelected) + { + pExt->UpdateIdleDir(); + return SkipGameCode; + } + } + + const auto pTypeExt = pExt->TypeExtData; + const auto currentMission = pThis->CurrentMission; + + // Busy in attacking or driver dead? + if (pThis->Target || (Unsorted::CurrentFrame - pThis->LastFireBulletFrame) < (RulesClass::Instance->GuardAreaTargetingDelay + 5) || (currentMission == Mission::Harmless && pThis->Owner == HouseClass::FindSpecial())) + { + pExt->CheckIdleAction(); + pExt->UpdateIdleDir(); + return SkipGameCode; + } + + const auto pWeaponStruct = pThis->GetTurretWeapon(); + auto shouldRotateInIdle = [pThis, pExt]() + { + if (!pThis->BunkerLinkedItem && pThis->Type->Speed && (!pThis->Type->IsSimpleDeployer || !pThis->Deployed)) + return true; + + pExt->UpdateIdleDir(); + return false; + }; + + // Turret locked? + if (pWeaponStruct && pWeaponStruct->WeaponType && pWeaponStruct->TurretLocked) + { + pExt->CheckIdleAction(); + + if (shouldRotateInIdle()) + pTypeExt->SetTurretLimitedDir(pThis, pThis->PrimaryFacing.Current()); + + return SkipGameCode; + } + + // Point to mouse + if (SessionClass::IsSingleplayer() && pThis->Owner->IsControlledByCurrentPlayer()) + { + if (pTypeExt->Turret_PointToMouse.Get(RulesExt::Global()->Turret_PointToMouse)) + pExt->ManualIdleAction(); + + if (pExt->UnitIdleIsSelected) + return SkipGameCode; + } + + const auto pDestination = pThis->Destination; + const auto pJumpjetLoco = locomotion_cast(pThis->Locomotor); + + if (pDestination && !pJumpjetLoco) + { + pExt->CheckIdleAction(); + + if (shouldRotateInIdle()) + pTypeExt->SetTurretLimitedDir(pThis, pThis->GetTargetDirection(pDestination)); + + return SkipGameCode; + } + + // Idle main + if (pTypeExt->Turret_IdleRotate.Get(RulesExt::Global()->Turret_IdleRotate)) + { + if (currentMission == Mission::Guard || (pJumpjetLoco && pJumpjetLoco->State == JumpjetLocomotionClass::State::Hovering)) + { + pExt->ApplyIdleAction(); + return SkipGameCode; + } + + pExt->StopIdleAction(); + } + + if (shouldRotateInIdle()) + pTypeExt->SetTurretLimitedDir(pThis, pThis->PrimaryFacing.Current()); + + return SkipGameCode; +} + +#pragma endregion + +#pragma region CheckFacing + +DEFINE_HOOK(0x7412BB, UnitClass_GetFireError_CheckFacingDeviation, 0x7) +{ + enum { SkipGameCode = 0x7412D4 }; + + GET(UnitClass* const, pThis, ESI); + GET(AbstractClass* const, pTarget, EBP); + GET(BulletTypeClass* const, pBulletType, EDX); + GET(DirStruct* const, pTgtDir, EAX); + + *pTgtDir = pThis->GetTargetDirection(pTarget); + + if (RulesExt::Global()->ExpandTurretRotation && pThis->Type->Turret) + *pTgtDir = TechnoTypeExt::ExtMap.Find(pThis->Type)->GetTurretDesiredDir(*pTgtDir); + + R->EBX(pBulletType->ROT ? 16 : 8); + return SkipGameCode; +} + +#pragma endregion diff --git a/src/Ext/Techno/Hooks.Misc.cpp b/src/Ext/Techno/Hooks.Misc.cpp index 517110b81a..48bc3d53e2 100644 --- a/src/Ext/Techno/Hooks.Misc.cpp +++ b/src/Ext/Techno/Hooks.Misc.cpp @@ -506,52 +506,6 @@ DEFINE_HOOK(0x74691D, UnitClass_UpdateDisguise_EMP, 0x6) #pragma endregion -#pragma region AttackMindControlledDelay - -bool __fastcall CanAttackMindControlled(TechnoClass* pControlled, TechnoClass* pRetaliator) -{ - const auto pMind = pControlled->MindControlledBy; - - if (!pMind || pRetaliator->Berzerk) - return true; - - const auto pManager = pMind->CaptureManager; - - if (!pManager || !pRetaliator->Owner->IsAlliedWith(pManager->GetOriginalOwner(pControlled))) - return true; - - return TechnoExt::ExtMap.Find(pControlled)->BeControlledThreatFrame <= Unsorted::CurrentFrame; -} - -DEFINE_HOOK(0x7089E8, TechnoClass_AllowedToRetaliate_AttackMindControlledDelay, 0x6) -{ - enum { CannotRetaliate = 0x708B17 }; - - GET(TechnoClass* const, pThis, ESI); - GET(TechnoClass* const, pAttacker, EBP); - - return CanAttackMindControlled(pAttacker, pThis) ? 0 : CannotRetaliate; -} - -DEFINE_HOOK(0x6F88BF, TechnoClass_CanAutoTargetObject_AttackMindControlledDelay, 0x6) -{ - enum { CannotSelect = 0x6F894F }; - - GET(ObjectClass* const, pTarget, ESI); - - if (const auto pTechno = abstract_cast(pTarget)) - { - GET(TechnoClass* const, pThis, EDI); - - if (!CanAttackMindControlled(pTechno, pThis)) - return CannotSelect; - } - - return 0; -} - -#pragma endregion - #pragma region ExtendedGattlingRateDown DEFINE_HOOK(0x70DE40, TechnoClass_GattlingValueRateDown_GattlingRateDownDelay, 0xA) diff --git a/src/Ext/Techno/Hooks.TargetEvaluation.cpp b/src/Ext/Techno/Hooks.TargetEvaluation.cpp index 10daf28aac..5048c97893 100644 --- a/src/Ext/Techno/Hooks.TargetEvaluation.cpp +++ b/src/Ext/Techno/Hooks.TargetEvaluation.cpp @@ -145,6 +145,87 @@ DEFINE_FUNCTION_JUMP(CALL6, 0x6F8DD2, TechnoClass_EvaluateCellGetWeaponRangeWrap #pragma endregion +#pragma region AutoTargetAndRetaliate + +bool __fastcall CanAttackMindControlled(TechnoClass* pControlled, TechnoClass* pRetaliator) +{ + const auto pMind = pControlled->MindControlledBy; + + if (!pMind || pRetaliator->Berzerk) + return true; + + const auto pManager = pMind->CaptureManager; + + if (!pManager || !pRetaliator->Owner->IsAlliedWith(pManager->GetOriginalOwner(pControlled))) + return true; + + return TechnoExt::ExtMap.Find(pControlled)->BeControlledThreatFrame <= Unsorted::CurrentFrame; +} + +DEFINE_HOOK(0x7089E8, TechnoClass_AllowedToRetaliate_AttackMindControlledDelay, 0x6) +{ + enum { CannotRetaliate = 0x708B17 }; + + GET(TechnoClass* const, pThis, ESI); + GET(TechnoClass* const, pAttacker, EBP); + + return CanAttackMindControlled(pAttacker, pThis) ? 0 : CannotRetaliate; +} + +static inline int CalculateExtraThreat(TechnoClass* pThis, ObjectClass* pTarget, int threat) +{ + const auto pTypeExt = TechnoExt::ExtMap.Find(pThis)->TypeExtData; + + if (!pTypeExt->TargetExtraThreat) + return threat; + + const auto& vec = pTypeExt->TargetExtraThreat_Multipliers; + const size_t multsCount = vec.size(); + + if (multsCount <= 0) + return threat; + + const size_t angleCount = pTypeExt->TargetExtraThreat_Angles.size(); + + if (angleCount <= 0) + return static_cast(threat * vec[0]); + + const auto absType = pThis->WhatAmI(); + const auto tgtDir = pThis->GetTargetDirection(pTarget); + const bool useSec = pTypeExt->TargetExtraThreat_Turret && absType == AbstractType::Unit && pTypeExt->OwnerObject()->Turret; + const auto curDir = (useSec || absType == AbstractType::Aircraft ? pThis->SecondaryFacing : pThis->PrimaryFacing).Current(); + const int difference = std::abs(static_cast(static_cast(tgtDir.Raw) - static_cast(curDir.Raw))); + + for (size_t i = 0; i < angleCount; ++i) + { + if (difference <= static_cast(pTypeExt->TargetExtraThreat_Angles[i].Raw)) + return static_cast(threat * vec[Math::min(i, (multsCount - 1))]); + } + + return static_cast(threat * vec[Math::min(angleCount, (multsCount - 1))]); +} + +DEFINE_HOOK(0x6F88BF, TechnoClass_CanAutoTargetObject_AttackMindControlledDelay, 0x6) +{ + enum { CannotSelect = 0x6F894F }; + + GET(TechnoClass* const, pThis, EDI); + GET(ObjectClass* const, pTarget, ESI); + GET(int* const, pThreat, EBP); + + if (const auto pTechno = abstract_cast(pTarget)) + { + if (!CanAttackMindControlled(pTechno, pThis)) + return CannotSelect; + } + + *pThreat = CalculateExtraThreat(pThis, pTarget, *pThreat); + + return 0; +} + +#pragma endregion + #pragma region AggressiveAttackMove static inline bool CheckAttackMoveCanResetTarget(FootClass* pThis) diff --git a/src/Ext/TechnoType/Body.cpp b/src/Ext/TechnoType/Body.cpp index f32b4669f9..9a5d867104 100644 --- a/src/Ext/TechnoType/Body.cpp +++ b/src/Ext/TechnoType/Body.cpp @@ -567,6 +567,125 @@ TechnoClass* TechnoTypeExt::CreateUnit(CreateUnitTypeClass* pCreateUnit, DirType return nullptr; } +DirStruct TechnoTypeExt::ExtData::GetTurretDesiredDir(DirStruct defaultDir) +{ + const auto turretExtraDir = this->Turret_ExtraAngle.Get(); + + if (!turretExtraDir.Raw) + return defaultDir; + + return DirStruct { static_cast(defaultDir.Raw) + static_cast(turretExtraDir.Raw) }; +} + +void TechnoTypeExt::ExtData::SetTurretLimitedDir(FootClass* pThis, DirStruct desiredDir) +{ + const auto turretRestrictDir = this->Turret_Restriction.Get(); + const auto pBody = &pThis->PrimaryFacing; + const auto pTurret = &pThis->SecondaryFacing; + const auto destinationDir = this->GetTurretDesiredDir(desiredDir); + auto setTurretDesired = [this, pBody, pTurret](const DirStruct& dir) + { + if (this->Turret_BodyFoundation.Get(RulesExt::Global()->Turret_BodyFoundation) && pBody->IsRotating()) + { + const auto difference = static_cast(pBody->Difference().Raw) > 0 ? pBody->ROT.Raw : -pBody->ROT.Raw; + const auto facing = DirStruct { pTurret->Current().Raw + difference }; + pTurret->DesiredFacing = facing; + pTurret->StartFacing = facing; + pTurret->RotationTimer.Start(0); + } + + pTurret->SetDesired(dir); + }; + // There are no restrictions + if (static_cast(turretRestrictDir.Raw) >= 32768) + { + setTurretDesired(destinationDir); + return; + } + + const auto restrictRaw = static_cast(turretRestrictDir.Raw); + const auto desiredRaw = static_cast(destinationDir.Raw); + const auto turretRaw = static_cast(pTurret->Current().Raw); + + auto currentDir = pBody->Current(); + auto bodyDir = this->GetTurretDesiredDir(currentDir); + auto bodyRaw = static_cast(bodyDir.Raw); + auto desiredDifference = static_cast(desiredRaw - bodyRaw); + // Beyond the rotation range of the turret, the body rotates first + if ((desiredDifference < -restrictRaw || desiredDifference > restrictRaw) && !pThis->Destination && !pThis->Locomotor->Is_Moving()) + { + pBody->SetDesired(this->Turret_BodyOrientation ? this->GetBodyDesiredDir(currentDir, desiredDir) : desiredDir); + // Once rotation begins, data needs to be updated to avoid delays + currentDir = pBody->Current(); + bodyDir = this->GetTurretDesiredDir(currentDir); + bodyRaw = static_cast(bodyDir.Raw); + desiredDifference = static_cast(desiredRaw - bodyRaw); + } + + const auto currentDifference = static_cast(turretRaw - bodyRaw); + // If the current orientation exceeds the limit, force it back to the maximum limit position + if (currentDifference < -restrictRaw) + { + const auto restrictDir = DirStruct { (bodyRaw - restrictRaw) }; + pTurret->SetCurrent(restrictDir); + pTurret->SetDesired(restrictDir); + } + else if (currentDifference > restrictRaw) + { + const auto restrictDir = DirStruct { (bodyRaw + restrictRaw) }; + pTurret->SetCurrent(restrictDir); + pTurret->SetDesired(restrictDir); + } + // When they are located on both sides of the body, first return the turret to its original position + if (currentDifference > 0 && desiredDifference < 0 || currentDifference < 0 && desiredDifference > 0) + setTurretDesired(bodyDir); + else if (desiredDifference < -restrictRaw) + setTurretDesired(DirStruct { (bodyRaw - restrictRaw) }); + else if (desiredDifference > restrictRaw) + setTurretDesired(DirStruct { (bodyRaw + restrictRaw) }); + else + setTurretDesired(destinationDir); +} + +short TechnoTypeExt::ExtData::GetTurretLimitedRaw(short currentDirectionRaw) +{ + const auto turretRestrictDir = this->Turret_Restriction.Get(); + + if (static_cast(turretRestrictDir.Raw) >= 32768) + return currentDirectionRaw; + + const auto restrictRaw = static_cast(turretRestrictDir.Raw); + + if (currentDirectionRaw < -restrictRaw) + return -restrictRaw; + + if (currentDirectionRaw > restrictRaw) + return restrictRaw; + + return currentDirectionRaw; +} + +DirStruct TechnoTypeExt::ExtData::GetBodyDesiredDir(DirStruct currentDir, DirStruct defaultDir) +{ + const auto bodyDir = this->Turret_BodyOrientationAngle.Get(); + + if (!bodyDir.Raw) + return defaultDir; + + const auto rotateRaw = static_cast(bodyDir.Raw); + const auto rotate = DirStruct { this->GetTurretLimitedRaw(rotateRaw) }; + const auto rightDir = DirStruct { static_cast(defaultDir.Raw) + static_cast(rotate.Raw) }; + + if (!this->Turret_BodyOrientationSymmetric) + return rightDir; + + const auto leftDir = DirStruct { static_cast(defaultDir.Raw) - static_cast(rotate.Raw) }; + const auto rightDifference = static_cast(static_cast(rightDir.Raw) - static_cast(currentDir.Raw)); + const auto leftDifference = static_cast(static_cast(leftDir.Raw) - static_cast(currentDir.Raw)); + + return (std::abs(rightDifference) < std::abs(leftDifference)) ? rightDir : leftDir; +} + // used for more WeaponX added by Ares. WeaponTypeClass* TechnoTypeExt::GetWeaponType(TechnoTypeClass* pThis, int weaponIndex, bool isElite) { @@ -854,6 +973,36 @@ void TechnoTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->BuildLimitGroup_ExtraLimit_MaxCount.Read(exINI, pSection, "BuildLimitGroup.ExtraLimit.MaxCount"); this->BuildLimitGroup_ExtraLimit_MaxNum.Read(exINI, pSection, "BuildLimitGroup.ExtraLimit.MaxNum"); + this->Turret_IdleRotate.Read(exINI, pSection, "Turret.IdleRotate"); + this->Turret_PointToMouse.Read(exINI, pSection, "Turret.PointToMouse"); + this->TurretROT.Read(exINI, pSection, "TurretROT"); + this->Turret_Restriction.Read(exINI, pSection, "Turret.Restriction"); + this->Turret_ExtraAngle.Read(exINI, pSection, "Turret.ExtraAngle"); + this->Turret_BodyFoundation.Read(exINI, pSection, "Turret.BodyFoundation"); + this->Turret_BodyOrientation.Read(exINI, pSection, "Turret.BodyOrientation"); + this->Turret_BodyOrientationAngle.Read(exINI, pSection, "Turret.BodyOrientationAngle"); + this->Turret_BodyOrientationSymmetric.Read(exINI, pSection, "Turret.BodyOrientationSymmetric"); + + this->TargetExtraThreat.Read(exINI, pSection, "TargetExtraThreat"); + { + ValueableVector ReadAngles {}; + ReadAngles.Read(exINI, pSection, "TargetExtraThreat.Angles"); + + if (const size_t count = ReadAngles.size()) + { + this->TargetExtraThreat_Angles.clear(); + this->TargetExtraThreat_Angles.resize(count); + + for (size_t i = 0; i < count; ++i) + { + const int raw = static_cast(ReadAngles[i] * (65536.0 / 360.0) + 0.5); + this->TargetExtraThreat_Angles[i] = DirStruct(std::clamp(raw, 0, 65535)); + } + } + } + this->TargetExtraThreat_Multipliers.Read(exINI, pSection, "TargetExtraThreat.Multipliers"); + this->TargetExtraThreat_Turret.Read(exINI, pSection, "TargetExtraThreat.Turret"); + this->AmphibiousEnter.Read(exINI, pSection, "AmphibiousEnter"); this->AmphibiousUnload.Read(exINI, pSection, "AmphibiousUnload"); this->NoQueueUpToEnter.Read(exINI, pSection, "NoQueueUpToEnter"); @@ -1476,6 +1625,21 @@ void TechnoTypeExt::ExtData::Serialize(T& Stm) .Process(this->BuildLimitGroup_ExtraLimit_MaxCount) .Process(this->BuildLimitGroup_ExtraLimit_MaxNum) + .Process(this->Turret_IdleRotate) + .Process(this->Turret_PointToMouse) + .Process(this->TurretROT) + .Process(this->Turret_Restriction) + .Process(this->Turret_ExtraAngle) + .Process(this->Turret_BodyFoundation) + .Process(this->Turret_BodyOrientation) + .Process(this->Turret_BodyOrientationAngle) + .Process(this->Turret_BodyOrientationSymmetric) + + .Process(this->TargetExtraThreat) + .Process(this->TargetExtraThreat_Angles) + .Process(this->TargetExtraThreat_Multipliers) + .Process(this->TargetExtraThreat_Turret) + .Process(this->AmphibiousEnter) .Process(this->AmphibiousUnload) .Process(this->NoQueueUpToEnter) diff --git a/src/Ext/TechnoType/Body.h b/src/Ext/TechnoType/Body.h index 75d46d3169..eb5c748c52 100644 --- a/src/Ext/TechnoType/Body.h +++ b/src/Ext/TechnoType/Body.h @@ -283,6 +283,21 @@ class TechnoTypeExt ValueableVector BuildLimitGroup_ExtraLimit_MaxCount; Valueable BuildLimitGroup_ExtraLimit_MaxNum; + Nullable Turret_IdleRotate; + Nullable Turret_PointToMouse; + Nullable TurretROT; + Valueable Turret_Restriction; + Valueable Turret_ExtraAngle; + Nullable Turret_BodyFoundation; + Valueable Turret_BodyOrientation; + Valueable Turret_BodyOrientationAngle; + Valueable Turret_BodyOrientationSymmetric; + + Valueable TargetExtraThreat; + ValueableVector TargetExtraThreat_Angles; + ValueableVector TargetExtraThreat_Multipliers; + Valueable TargetExtraThreat_Turret; + Nullable AmphibiousEnter; Nullable AmphibiousUnload; Nullable NoQueueUpToEnter; @@ -670,6 +685,21 @@ class TechnoTypeExt , BuildLimitGroup_ExtraLimit_MaxCount {} , BuildLimitGroup_ExtraLimit_MaxNum { 0 } + , Turret_IdleRotate {} + , Turret_PointToMouse {} + , TurretROT {} + , Turret_Restriction { DirStruct(32768) } + , Turret_ExtraAngle { DirStruct(0) } + , Turret_BodyFoundation {} + , Turret_BodyOrientation { false } + , Turret_BodyOrientationAngle { DirStruct(0) } + , Turret_BodyOrientationSymmetric { true } + + , TargetExtraThreat { false } + , TargetExtraThreat_Angles {} + , TargetExtraThreat_Multipliers {} + , TargetExtraThreat_Turret { true } + , AmphibiousEnter {} , AmphibiousUnload {} , NoQueueUpToEnter {} @@ -805,6 +835,11 @@ class TechnoTypeExt int SelectForceWeapon(TechnoClass* pThis, AbstractClass* pTarget); int SelectMultiWeapon(TechnoClass* const pThis, AbstractClass* const pTarget); + DirStruct GetTurretDesiredDir(DirStruct defaultDir); + void SetTurretLimitedDir(FootClass* pThis, DirStruct desiredDir); + short GetTurretLimitedRaw(short currentDirectionRaw); + DirStruct GetBodyDesiredDir(DirStruct currentDir, DirStruct defaultDir); + // Ares 0.A const char* GetSelectionGroupID() const; diff --git a/src/Ext/Unit/Hooks.Jumpjet.cpp b/src/Ext/Unit/Hooks.Jumpjet.cpp index d63ed930af..cf6e00cfc8 100644 --- a/src/Ext/Unit/Hooks.Jumpjet.cpp +++ b/src/Ext/Unit/Hooks.Jumpjet.cpp @@ -22,7 +22,25 @@ DEFINE_HOOK(0x736F78, UnitClass_UpdateFiring_FireErrorIsFACING, 0x6) if (pType->Turret && !pType->HasTurret) // 0x736F92 { - pThis->SecondaryFacing.SetDesired(tgtDir); + if (RulesExt::Global()->ExpandTurretRotation) + { + const auto pTypeExt = TechnoTypeExt::ExtMap.Find(pThis->Type); + + if (pTypeExt->Turret_BodyOrientation && !pThis->Destination && !pThis->Locomotor->Is_Moving()) + { + const auto curDir = pThis->PrimaryFacing.Current(); + const auto dir = pTypeExt->GetBodyDesiredDir(curDir, tgtDir); + + if (std::abs(static_cast(static_cast(dir.Raw) - static_cast(curDir.Raw))) >= 8192) + pThis->PrimaryFacing.SetDesired(dir); + } + + pTypeExt->SetTurretLimitedDir(pThis, tgtDir); + } + else + { + pThis->SecondaryFacing.SetDesired(tgtDir); + } } else // 0x736FB6 { diff --git a/src/Utilities/TemplateDef.h b/src/Utilities/TemplateDef.h index a533f4e65f..34a7661751 100644 --- a/src/Utilities/TemplateDef.h +++ b/src/Utilities/TemplateDef.h @@ -513,6 +513,21 @@ namespace detail return false; } + template <> + inline bool read(DirStruct& value, INI_EX& parser, const char* pSection, const char* pKey) + { + double buffer; + + if (parser.ReadDouble(pSection, pKey, &buffer)) + { + const int raw = static_cast(buffer * (65536.0 / 360.0) + 0.5); + value = DirStruct(std::clamp(raw, -65535, 65535)); + return true; + } + + return false; + } + template <> inline bool read(DirType& value, INI_EX& parser, const char* pSection, const char* pKey) {