diff --git a/CREDITS.md b/CREDITS.md index 011372db13..64161b4618 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -291,6 +291,7 @@ This page lists all the individual contributions to the project by their author. - Wall overlay unit sell exploit fix - Fix vehicles disguised as trees incorrectly displaying veterancy insignia when they shouldn't - GapGen + SpySat desync fix + - 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 91a3fc34ff..3aeff559b1 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -1002,6 +1002,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 d0e3ab9d8c..6e53828c47 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -568,6 +568,7 @@ New: - [Technos with Walk locomotor spawn wake like ship](Fixed-or-Improved-Logics.md#customizable-wake-anim) (by TaranDahl) - [Hotkey for deselect object from current selection](User-Interface.md#deselect-object) (by FrozenFog) - [Updateable firing anim](Fixed-or-Improved-Logics.md#updateable-firing-anim) (by TaranDahl) +- [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 0405c71501..4e59e4c157 100644 --- a/src/Ext/Building/Body.cpp +++ b/src/Ext/Building/Body.cpp @@ -455,6 +455,93 @@ 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; + int facing = pThis->PrimaryFacing.Current().GetValue<5>(); + int shapeFacing = ObjectClass::BodyShape[facing]; + + bool isLowPower = !pThis->StuffEnabled || !pThis->IsPowerOnline(); + bool isFiring = pExt->TurretAnimFiringFrame != -1; + + int idleBlockSize = 32 * pTypeExt->TurretAnim_IdleFrames; + int lowPowerIdleBlockSize = 32 * pTypeExt->TurretAnim_LowPowerIdleFrames; + int firingBlockSize = 32 * pTypeExt->TurretAnim_FiringFrames; + int offsetIdle = 0; + int offsetLowPowerIdle = offsetIdle + idleBlockSize; + int offsetFiring = offsetLowPowerIdle + lowPowerIdleBlockSize; + int offsetLowPowerFiring = offsetFiring + firingBlockSize; + + int framesPerFacing = pTypeExt->TurretAnim_IdleFrames; + int baseOffset = offsetIdle; + bool hasFiringFrames = false; + + if (isLowPower) + { + if (isFiring && pTypeExt->TurretAnim_LowPowerFiringFrames > 0) + { + framesPerFacing = pTypeExt->TurretAnim_LowPowerFiringFrames; + baseOffset = offsetLowPowerFiring; + hasFiringFrames = true; + } + else if (pTypeExt->TurretAnim_LowPowerIdleFrames > 0) + { + framesPerFacing = pTypeExt->TurretAnim_LowPowerIdleFrames; + baseOffset = offsetLowPowerIdle; + } + } + else + { + if (isFiring && pTypeExt->TurretAnim_FiringFrames > 0) + { + framesPerFacing = pTypeExt->TurretAnim_FiringFrames; + baseOffset = offsetFiring; + 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 @@ -474,6 +561,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. ; } @@ -505,7 +595,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 215d284034..960675a9cd 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 6f3b790408..dc105c7bfe 100644 --- a/src/Ext/Building/Hooks.cpp +++ b/src/Ext/Building/Hooks.cpp @@ -1087,3 +1087,48 @@ DEFINE_HOOK(0x45670D, BuildingClass_GetRadialIndicatorRange_Extras, 0x7) R->EAX(pThis->TechnoClass::GetTurretWeapon()); return ApplyTurretWeapon; } + +#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; + bool isLowPower = !pThis->StuffEnabled || !pThis->IsPowerOnline(); + bool firingFrames = isLowPower ? pTypeExt->TurretAnim_LowPowerFiringFrames : pTypeExt->TurretAnim_FiringFrames; + + if (firingFrames > 0 && pExt->TurretAnimFiringFrame == -1) + { + pExt->TurretAnimFiringFrame = 0; + pExt->TurretAnimRateTick = 0; + } + } + } + + return 0; +} + +#pragma endregion diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index 0df69a20b0..aa62b2871d 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -139,6 +139,7 @@ int BuildingTypeExt::GetUpgradesAmount(BuildingTypeClass* pBuilding, HouseClass* return isUpgrade ? result : -1; } + void BuildingTypeExt::ExtData::Initialize() { } @@ -223,6 +224,14 @@ void BuildingTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->UndeploysInto_Sellable.Read(exINI, pSection, "UndeploysInto.Sellable"); this->BuildingRadioLink_SyncOwner.Read(exINI, pSection, "BuildingRadioLink.SyncOwner"); + // Existing TurretAnim characteristics are read from rules so following the pattern here. + 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->NumberOfDocks > 0) { std::optional empty; @@ -378,6 +387,12 @@ void BuildingTypeExt::ExtData::Serialize(T& Stm) .Process(this->HasPowerUpAnim) .Process(this->UndeploysInto_Sellable) .Process(this->BuildingRadioLink_SyncOwner) + .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 81fde368c3..f988cca271 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -104,6 +104,13 @@ class BuildingTypeExt Nullable BuildingRadioLink_SyncOwner; + Valueable TurretAnim_IdleFrames; + Valueable TurretAnim_LowPowerIdleFrames; + Valueable TurretAnim_FiringFrames; + Valueable TurretAnim_LowPowerFiringFrames; + Valueable TurretAnim_IdleRate; + Valueable TurretAnim_FiringRate; + // Ares 0.2 Valueable CloningFacility; @@ -183,6 +190,12 @@ class BuildingTypeExt , HasPowerUpAnim {} , UndeploysInto_Sellable { false } , BuildingRadioLink_SyncOwner {} + , TurretAnim_IdleFrames { 1 } + , TurretAnim_LowPowerIdleFrames { 0 } + , TurretAnim_FiringFrames { 0 } + , TurretAnim_LowPowerFiringFrames { 0 } + , TurretAnim_IdleRate { 1 } + , TurretAnim_FiringRate { 1 } // Ares 0.2 , CloningFacility { false }