diff --git a/CREDITS.md b/CREDITS.md index ad0f227ff4..4090c19e99 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -294,6 +294,7 @@ This page lists all the individual contributions to the project by their author. - Frame CRC generation rewrite - Laser drawing Z-adjust customization - Armed building guard mission retry delay customization + - Building turret idle/firing/low power animations - **Morton (MortonPL)**: - `XDrawOffset` for animations - Shield passthrough & absorption diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 66f149e94f..16e550861b 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1026,6 +1026,24 @@ In `rulesmd.ini`: ConsideredVehicle= ; boolean ``` +### Building turret animations + +- By default building `TurretAnim(Damaged)` with `TurretAnimIsVoxel=false` only displays one frame per each of the 32 facings. This can now be increased and there are additional animations available for low power state and firing weapons. + - The frames in the .shp file should be in the order: `IdleFrames`, `LowPowerIdleFrames`, `FiringFrames`, `LowPowerFiringFrames`, animations with frame count set to 0 will be skipped / ignored. + - Note that `FiringFrames` starts playing when attacking and weapon can fire, it will not stop firing of weapon until it has finished playing nor will anything prevent it from looping multiple times if weapon firing is blocked by [delayed firing](New-or-Enhanced-Logics.md#delayed-firing) for longer than there are frames for. Matching delayed firing duration with firing frame count can be used to make pre-firing animation. + - `TurretAnim.IdleRate` and `TurretAnim.FiringRate` can be used to customize animation frame playback rate for idle and firing frames respectively. + +In `rulesmd.ini`: +```ini +[SOMEBUILDING] ; BuildingType +TurretAnim.IdleFrames=1 ; integer +TurretAnim.LowPowerIdleFrames=0 ; integer +TurretAnim.FiringFrames=0 ; integer +TurretAnim.LowPowerFiringFrames=0 ; integer +TurretAnim.IdleRate=1 ; integer, game frames +TurretAnim.FiringRate=1 ; integer, game frames +``` + ### Custom exit cell for infantry factory - By default `Factory=InfantryType` buildings use exit cell for the created infantry based on hardcoded settings if any of `GDIBarracks`, `NODBarracks` or `YuriBarracks` are set to true. It is now possible to define arbitrary exit cell for such building via `BarracksExitCell`. Below is a reference of the cell offsets for the hardcoded values. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 9dbeac9ec9..afbb8f51b6 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -582,6 +582,7 @@ HideShakeEffects=false ; boolean - Restored the original Tiberian Sun behavior of playing the `[AudioVisual] -> DeploySound=` sound effect when clicking the sidebar to execute `Deploy` (by Noble_Fish) - Allow `RemoveMindControl` warhead to mute `MindClearedSound` (by Noble_Fish) - Introduce weight selection rules for ExtraWarheads (by Noble_Fish) +- [Building turret idle/firing/low power animations](Fixed-or-Improved-Logics.md#building-turret-animations) (by Starkku) #### 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/Building/Body.cpp b/src/Ext/Building/Body.cpp index f773e2e741..af9b7a5bf4 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -461,6 +461,89 @@ void BuildingExt::KickOutClone(std::pair& info, v pClone->UnInit(); } +int BuildingExt::GetTurretFrame(BuildingClass* pThis) +{ + auto const pExt = BuildingExt::ExtMap.Find(pThis); + auto const pTypeExt = pExt->TypeExtData; + const int facing = pThis->PrimaryFacing.Current().GetValue<5>(); + const int shapeFacing = ObjectClass::BodyShape[facing]; + + const bool isLowPower = !pThis->StuffEnabled || !pThis->IsPowerOnline(); + const bool isFiring = pExt->TurretAnimFiringFrame != -1; + + const int idleBlockSize = 32 * pTypeExt->TurretAnim_IdleFrames; + const int lowPowerIdleBlockSize = 32 * pTypeExt->TurretAnim_LowPowerIdleFrames; + const int firingBlockSize = 32 * pTypeExt->TurretAnim_FiringFrames; + + int framesPerFacing = pTypeExt->TurretAnim_IdleFrames; + int baseOffset = 0; + bool hasFiringFrames = false; + + if (isLowPower) + { + if (isFiring && pTypeExt->TurretAnim_LowPowerFiringFrames > 0) + { + framesPerFacing = pTypeExt->TurretAnim_LowPowerFiringFrames; + baseOffset = idleBlockSize + lowPowerIdleBlockSize + firingBlockSize; + hasFiringFrames = true; + } + else if (pTypeExt->TurretAnim_LowPowerIdleFrames > 0) + { + framesPerFacing = pTypeExt->TurretAnim_LowPowerIdleFrames; + baseOffset = idleBlockSize; + } + } + else + { + if (isFiring && pTypeExt->TurretAnim_FiringFrames > 0) + { + framesPerFacing = pTypeExt->TurretAnim_FiringFrames; + baseOffset = idleBlockSize + lowPowerIdleBlockSize; + hasFiringFrames = true; + } + } + + int animFrame = 0; + + if (isFiring && hasFiringFrames) + { + animFrame = pExt->TurretAnimFiringFrame; + pExt->TurretAnimRateTick++; + + if (pExt->TurretAnimRateTick >= pTypeExt->TurretAnim_FiringRate) + { + pExt->TurretAnimRateTick = 0; + pExt->TurretAnimFiringFrame++; + } + + if (pExt->TurretAnimFiringFrame >= framesPerFacing) + { + pExt->TurretAnimFiringFrame = -1; + pExt->TurretAnimIdleFrame = 0; // Reset idle anim frame. + pExt->TurretAnimRateTick = 0; + } + } + else if (framesPerFacing > 1) + { + animFrame = pExt->TurretAnimIdleFrame; + pExt->TurretAnimRateTick++; + + if (pExt->TurretAnimRateTick >= pTypeExt->TurretAnim_IdleRate) + { + pExt->TurretAnimRateTick = 0; + pExt->TurretAnimIdleFrame++; + } + + if (pExt->TurretAnimIdleFrame >= framesPerFacing) + { + pExt->TurretAnimIdleFrame = 0; + pExt->TurretAnimRateTick = 0; + } + } + + return baseOffset + (shapeFacing * framesPerFacing) + animFrame; +} + // ============================= // load / save @@ -480,6 +563,9 @@ void BuildingExt::ExtData::Serialize(T& Stm) .Process(this->CurrentLaserWeaponIndex) .Process(this->PoweredUpToLevel) .Process(this->CurrentEMPulseSW) + .Process(this->TurretAnimIdleFrame) + .Process(this->TurretAnimFiringFrame) + .Process(this->TurretAnimRateTick) //.Process(this->IsFiringNow) It is set and reset within a same function. ; } @@ -511,7 +597,7 @@ bool BuildingExt::SaveGlobals(PhobosStreamWriter& Stm) // ============================= // container -BuildingExt::ExtContainer::ExtContainer() : Container("BuildingClass") { } +BuildingExt::ExtContainer::ExtContainer() : Container("BuildingClass") {} BuildingExt::ExtContainer::~ExtContainer() = default; diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index f54e1538e4..2539c353b9 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -27,6 +27,9 @@ class BuildingExt int PoweredUpToLevel; // Distinct from UpgradeLevel, and set to highest PowersUpToLevel out of applied upgrades regardless of how many are currently applied to this building. SuperClass* CurrentEMPulseSW; bool IsFiringNow; + int TurretAnimIdleFrame; + int TurretAnimFiringFrame; + int TurretAnimRateTick; ExtData(BuildingClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } @@ -42,6 +45,9 @@ class BuildingExt , PoweredUpToLevel { 0 } , CurrentEMPulseSW {} , IsFiringNow { false } + , TurretAnimIdleFrame { 0 } + , TurretAnimFiringFrame { -1 } + , TurretAnimRateTick { 0 } { } void DisplayIncomeString(); @@ -102,4 +108,5 @@ class BuildingExt static const std::vector GetFoundationCells(BuildingClass* pThis, CellStruct baseCoords, bool includeOccupyHeight = false); static WeaponStruct* GetLaserWeapon(BuildingClass* pThis); static void __fastcall KickOutClone(std::pair& info, void*, BuildingClass* pFactory); + static int GetTurretFrame(BuildingClass* pThis); }; diff --git a/src/Ext/Building/Hooks.cpp b/src/Ext/Building/Hooks.cpp index 0898bb21ed..a870339117 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1127,3 +1127,46 @@ DEFINE_HOOK(0x4496FB, BuildingClass_Mission_Guard_Armed, 0x6) } #pragma endregion + +#pragma region TurretAnim + +DEFINE_HOOK(0x451242, BuildingClass_AnimationAI_TurretAnim, 0xA) +{ + enum { SkipGameCode = 0x451296 }; + + GET(BuildingClass*, pThis, ESI); + + if (auto const pAnim = pThis->Anims[(int)BuildingAnimSlot::Turret]) + { + pAnim->Animation.Value = BuildingExt::GetTurretFrame(pThis); + pAnim->Animation.Step = 0; + } + + return SkipGameCode; +} + +DEFINE_HOOK(0x44B6C7, BuildingClass_Mission_Attack_TurretAnim, 0x6) +{ + enum { SkipFiring = 0x44B6FE }; + + GET(BuildingClass*, pThis, ESI); + + if (pThis->HasTurret()) + { + if (auto const pAnim = pThis->Anims[(int)BuildingAnimSlot::Turret]) + { + auto const pExt = BuildingExt::ExtMap.Find(pThis); + auto const pTypeExt = pExt->TypeExtData; + const bool isLowPower = !pThis->StuffEnabled || !pThis->IsPowerOnline(); + const int firingFrames = isLowPower ? pTypeExt->TurretAnim_LowPowerFiringFrames : pTypeExt->TurretAnim_FiringFrames; + + if (firingFrames > 0 && pExt->TurretAnimFiringFrame == -1) + { + pExt->TurretAnimFiringFrame = 0; + pExt->TurretAnimRateTick = 0; + } + } + } + + return 0; +} diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 83cae3cb2d..3f99b86b28 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -146,6 +146,7 @@ int BuildingTypeExt::GetUpgradesAmount(BuildingTypeClass* pBuilding, HouseClass* return isUpgrade ? result : -1; } + void BuildingTypeExt::ExtData::Initialize() { } @@ -233,6 +234,13 @@ void BuildingTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->BuildingRadioLink_SyncOwner.Read(exINI, pSection, "BuildingRadioLink.SyncOwner"); this->GuardRetryDelay.Read(exINI, pSection, "GuardRetryDelay"); + this->TurretAnim_IdleFrames.Read(exINI, pSection, "TurretAnim.IdleFrames"); + this->TurretAnim_LowPowerIdleFrames.Read(exINI, pSection, "TurretAnim.LowPowerIdleFrames"); + this->TurretAnim_FiringFrames.Read(exINI, pSection, "TurretAnim.FiringFrames"); + this->TurretAnim_LowPowerFiringFrames.Read(exINI, pSection, "TurretAnim.LowPowerFiringFrames"); + this->TurretAnim_IdleRate.Read(exINI, pSection, "TurretAnim.IdleRate"); + this->TurretAnim_FiringRate.Read(exINI, pSection, "TurretAnim.FiringRate"); + if (pThis->PowersUpBuilding[0] == NULL && this->PowersUp_Buildings.size() > 0) { strcpy_s(pThis->PowersUpBuilding, this->PowersUp_Buildings[0]->ID); @@ -402,6 +410,12 @@ void BuildingTypeExt::ExtData::Serialize(T& Stm) .Process(this->UndeploysInto_Sellable) .Process(this->BuildingRadioLink_SyncOwner) .Process(this->GuardRetryDelay) + .Process(this->TurretAnim_IdleFrames) + .Process(this->TurretAnim_LowPowerIdleFrames) + .Process(this->TurretAnim_FiringFrames) + .Process(this->TurretAnim_LowPowerFiringFrames) + .Process(this->TurretAnim_IdleRate) + .Process(this->TurretAnim_FiringFrames) // Ares 0.2 .Process(this->CloningFacility) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 47fce0c4f8..e112f93333 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -107,6 +107,13 @@ class BuildingTypeExt Nullable> GuardRetryDelay; + Valueable TurretAnim_IdleFrames; + Valueable TurretAnim_LowPowerIdleFrames; + Valueable TurretAnim_FiringFrames; + Valueable TurretAnim_LowPowerFiringFrames; + Valueable TurretAnim_IdleRate; + Valueable TurretAnim_FiringRate; + // Ares 0.2 Valueable CloningFacility; @@ -188,6 +195,12 @@ class BuildingTypeExt , UndeploysInto_Sellable { false } , BuildingRadioLink_SyncOwner {} , GuardRetryDelay {} + , TurretAnim_IdleFrames { 1 } + , TurretAnim_LowPowerIdleFrames { 0 } + , TurretAnim_FiringFrames { 0 } + , TurretAnim_LowPowerFiringFrames { 0 } + , TurretAnim_IdleRate { 1 } + , TurretAnim_FiringRate { 1 } // Ares 0.2 , CloningFacility { false }