diff --git a/CREDITS.md b/CREDITS.md index 919ed36608..56e13a8c71 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -741,6 +741,7 @@ This page lists all the individual contributions to the project by their author. - Disable AlphaImage during Buildup - Allow customizing the default value of `[Warhead] -> PreventScatter` via `[CombatDamage] -> Warhead.PreventScatter` - Allow `(Pre)ProductionAnim` animations to use `Powered` & `PoweredLight/Effect/Special` keys + - `RoofProductionAnim` - **Ollerus**: - Build limit group enhancement - Customizable rocker amplitude diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 2266f70d42..83b0323d20 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -741,6 +741,27 @@ PowerPlantEnhancer.Factor=1.0 ; floating point value PowerPlantEnhancer.MaxCount=-1 ; integer ``` +### Roof production anim + +- Now, you can use the `RoofProductionAnim*` series of flags to replace the `ProductionAnim*` series of flags when the produced infantry and vehicles are leaving the factory through the roof hatch. + - The criterion for exiting through the roof hatch is the same as `RoofDeployingAnim` in vanilla: the techno has `JumpJet=yes` or `BalloonHover=yes`. + +In `artmd.ini`: +```ini +[SOMEBUILDING] ; BuildingType, with Factory=InfantryType or Factory=UnitType +RoofProductionAnim= ; AnimationType, falls back to ProductionAnim if unset +RoofProductionAnimDamaged= ; AnimationType, falls back to RoofProductionAnim if unset +RoofProductionAnimGarrisoned= ; AnimationType, falls back to RoofProductionAnim if unset +RoofProductionAnimX= ; integer, defaults to ProductionAnimX +RoofProductionAnimY= ; integer, defaults to ProductionAnimY +RoofProductionAnimYSort= ; integer, defaults to ProductionAnimYSort +RoofProductionAnimZAdjust= ; integer, defaults to ProductionAnimZAdjust +RoofProductionAnimPowered= ; boolean, defaults to ProductionAnimPowered +RoofProductionAnimPoweredLight= ; boolean, defaults to ProductionAnimPoweredLight +RoofProductionAnimPoweredEffect= ; boolean, defaults to ProductionAnimPoweredEffect +RoofProductionAnimPoweredSpecial= ; boolean, defaults to ProductionAnimPoweredSpecial +``` + ### Spy effects - Additional espionage bonuses can be toggled with `SpyEffect.Custom`. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index b5dcd663cf..257b0b3943 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -419,6 +419,7 @@ HideShakeEffects=false ; boolean :open: #### New: +- [`RoofProductionAnim`](New-or-Enhanced-Logics.md#roof-production-anim) (by Noble_Fish) #### Vanilla fixes: diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index 6a92c5b18e..f4dd0b2a16 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -2535,6 +2535,9 @@ msgstr "" "允许 `(Pre)ProductionAnim` 动画使用 `Powered` 与 `PoweredLight/Effect/Special` " "类的标签" +msgid "`RoofProductionAnim`" +msgstr "`RoofProductionAnim`" + msgid "**Ollerus**:" msgstr "**Ollerus**:" diff --git a/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po b/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po index e339391b69..98487f55d7 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po +++ b/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po @@ -1871,6 +1871,22 @@ msgid "" "means no limitation." msgstr "`PowerPlantEnhancer.MaxCount` 决定了此类建筑中参与电厂增幅的数量上限。负值代表无上限。" +msgid "Roof production anim" +msgstr "天窗生产动画" + +msgid "" +"Now, you can use the `RoofProductionAnim*` series of flags to replace the `ProductionAnim*` series of flags when the produced infantry and vehicles are leaving the factory through the roof hatch." +msgstr "" +"现在,你可以用 `RoofProductionAnim*` 系列语句在所生产的步兵和载具从天窗离开工厂时替代 `ProductionAnim*` 系列语句使用。" + +msgid "" +"The criterion for exiting through the roof hatch is the same as " +"`RoofDeployingAnim` in vanilla: the unit has `Jumpjet=yes` or " +"`BalloonHover=yes`." +msgstr "" +"从天窗离开的判定标准与原版中 `RoofDeployingAnim` 相同:单位拥有 `Jumpjet=yes` 或 " +"`BalloonHover=yes`。" + msgid "Spy effects" msgstr "间谍效果" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index 68d897ef41..a7db19148e 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -2495,6 +2495,13 @@ msgstr "" "允许 `(Pre)ProductionAnim` 动画使用 `Powered` 与 `PoweredLight/Effect/Special` " "类的标签(by Noble_Fish)" +msgid "" +"[`RoofProductionAnim`](New-or-Enhanced-Logics.md#roof-production-anim) " +"(by Noble_Fish)" +msgstr "" +"[`RoofProductionAnim`](New-or-Enhanced-Logics.md#roof-production-anim)(by" +" Noble_Fish)" + msgid "Vanilla fixes:" msgstr "原版问题修复:" diff --git a/src/Ext/Building/Body.h b/src/Ext/Building/Body.h index a7a674c6c6..a756dab4f9 100644 --- a/src/Ext/Building/Body.h +++ b/src/Ext/Building/Body.h @@ -28,6 +28,7 @@ class BuildingExt final : public TechnoExt, public Detach::ListenerConstructionYard) return true; - if (pType->Factory == AbstractType::BuildingType && GeneralUtils::IsValidString(pType->BuildingAnim[(int)BuildingAnimSlot::Production].Anim)) + if (pType->Factory == AbstractType::BuildingType && GeneralUtils::IsValidString(pType->GetBuildingAnim(BuildingAnimSlot::Production).Anim)) return true; return false; } +static bool IsRoofExitTechno(TechnoTypeClass* pType) +{ + return pType->JumpJet || pType->BalloonHover; +} + +static bool IsRoofExitBuildingUnit(BuildingClass* pBuilding) +{ + auto pUnit = pBuilding->GetNthLink(); + return pUnit && IsRoofExitTechno(pUnit->GetTechnoType()); +} + +static AnimTypeClass* PickRoofProductionAnim(BuildingTypeExt* pTypeExt, bool isDamaged, bool garrisoned) +{ + if (garrisoned && pTypeExt->RoofProductionAnimGarrisoned.Get() != nullptr) + return pTypeExt->RoofProductionAnimGarrisoned.Get(); + + if (isDamaged && pTypeExt->RoofProductionAnimDamaged.Get() != nullptr) + return pTypeExt->RoofProductionAnimDamaged.Get(); + + return pTypeExt->RoofProductionAnim.Get(); +} + +static void PlayRoofProductionAnim(BuildingClass* pBuilding, BuildingTypeExt* pTypeExt, bool isDamaged, bool garrisoned) +{ + const char* animName = nullptr; + + if (auto pAnimType = PickRoofProductionAnim(pTypeExt, isDamaged, garrisoned)) + { + animName = pAnimType->get_ID(); + } + else + { + // Fall back to the plain ProductionAnim only, so the roof line stays independent + // of the ProductionAnimDamaged/Garrisoned branches. + animName = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production).Anim; + } + + if (!GeneralUtils::IsValidString(animName)) + return; + + // The roof anim needs its own placement and power values without affecting + // the plain line, so apply them only for the duration of this PlayAnim + // call. + auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); + + const Point2D savedPosition = prodAnim.Position; + const int savedZAdjust = prodAnim.ZAdjust; + const int savedYSort = prodAnim.YSort; + const bool savedPowered = prodAnim.Powered; + const bool savedPoweredLight = prodAnim.PoweredLight; + const bool savedPoweredEffect = prodAnim.PoweredEffect; + const bool savedPoweredSpecial = prodAnim.PoweredSpecial; + + prodAnim.Position.X = pTypeExt->RoofProductionAnimX.Get(savedPosition.X); + prodAnim.Position.Y = pTypeExt->RoofProductionAnimY.Get(savedPosition.Y); + prodAnim.ZAdjust = pTypeExt->RoofProductionAnimZAdjust.Get(savedZAdjust); + prodAnim.YSort = pTypeExt->RoofProductionAnimYSort.Get(savedYSort); + prodAnim.Powered = pTypeExt->RoofProductionAnimPowered.Get(savedPowered); + prodAnim.PoweredLight = pTypeExt->RoofProductionAnimPoweredLight.Get(savedPoweredLight); + prodAnim.PoweredEffect = pTypeExt->RoofProductionAnimPoweredEffect.Get(savedPoweredEffect); + prodAnim.PoweredSpecial = pTypeExt->RoofProductionAnimPoweredSpecial.Get(savedPoweredSpecial); + + auto pExt = BuildingExt::Fetch(pBuilding); + pExt->IsPlayingRoofProductionAnim = true; + pBuilding->PlayAnim(animName, BuildingAnimSlot::Production, isDamaged, garrisoned, 0); + pExt->IsPlayingRoofProductionAnim = false; + + prodAnim.Position = savedPosition; + prodAnim.ZAdjust = savedZAdjust; + prodAnim.YSort = savedYSort; + prodAnim.Powered = savedPowered; + prodAnim.PoweredLight = savedPoweredLight; + prodAnim.PoweredEffect = savedPoweredEffect; + prodAnim.PoweredSpecial = savedPoweredSpecial; +} + DEFINE_HOOK(0x43CC73, BuildingClass_ReceiveMessage_ProductionAnim, 0x6) { enum { SkipGameCode = 0x43CC79 }; @@ -1052,23 +1128,70 @@ DEFINE_HOOK(0x44B7AE, BuildingClass_Mission_Repair_ProductionAnim, 0x6) return SkipGameCode; } +// isRoofExit tells whether the produced unit leaves through the roof hatch; +// each caller decides it from its own reliable source. +static bool TryPlayRoofProductionAnim(BuildingClass* pBuilding, bool isDamaged, bool isRoofExit) +{ + if (!isRoofExit) + return false; + + auto pTypeExt = BuildingTypeExt::Fetch(pBuilding->Type); + const bool garrisoned = pBuilding->GetOccupantCount() > 0; + + const auto& prodAnim = pBuilding->Type->GetBuildingAnim(BuildingAnimSlot::Production); + if (BuildingTypeExt::IsPoweredAnimBlocked(pBuilding, + pTypeExt->RoofProductionAnimPowered.Get(prodAnim.Powered), + pTypeExt->RoofProductionAnimPoweredLight.Get(prodAnim.PoweredLight), + pTypeExt->RoofProductionAnimPoweredEffect.Get(prodAnim.PoweredEffect), + pTypeExt->RoofProductionAnimPoweredSpecial.Get(prodAnim.PoweredSpecial))) + return true; + + PlayRoofProductionAnim(pBuilding, pTypeExt, isDamaged, garrisoned); + return true; +} + +DEFINE_HOOK(0x44DDF0, BuildingClass_Unload_RoofProductionAnim, 0x6) +{ + enum { SkipGameCode = 0x44E267 }; + + GET(BuildingClass*, pBuilding, EBP); + + return TryPlayRoofProductionAnim(pBuilding, false, IsRoofExitBuildingUnit(pBuilding)) ? SkipGameCode : 0; +} + +DEFINE_HOOK(0x44DDDE, BuildingClass_Unload_RoofProductionAnim_Damaged, 0x6) +{ + enum { SkipGameCode = 0x44E267 }; + + GET(BuildingClass*, pBuilding, EBP); + + return TryPlayRoofProductionAnim(pBuilding, true, IsRoofExitBuildingUnit(pBuilding)) ? SkipGameCode : 0; +} + DEFINE_HOOK(0x444D11, BuildingClass_ExitObject_ProductionAnimForInfantryFactory, 0x6) { GET(BuildingClass*, pThis, ESI); + // The roof check must use the unit that is actually leaving the factory. + GET(TechnoClass*, pUnit, EDI); auto const pType = pThis->Type; if (pType->Factory == AbstractType::InfantryType) { - bool isDamaged = false; - auto anim = pType->BuildingAnim[(int)BuildingAnimSlot::Production].Anim; + const bool isDamaged = pThis->GetHealthPercentage() <= RulesClass::Instance->ConditionYellow; - if (pThis->GetHealthPercentage() <= RulesClass::Instance->ConditionYellow) + if (pUnit && IsRoofExitTechno(pUnit->GetTechnoType())) { - isDamaged = true; - anim = pType->BuildingAnim[(int)BuildingAnimSlot::Production].Damaged; + pThis->DestroyNthAnim(BuildingAnimSlot::Idle); + TryPlayRoofProductionAnim(pThis, isDamaged, true); + return 0; } + auto anim = pType->GetBuildingAnim(BuildingAnimSlot::Production).Anim; + + if (isDamaged) + anim = pType->GetBuildingAnim(BuildingAnimSlot::Production).Damaged; + if (GeneralUtils::IsValidString(anim)) { pThis->DestroyNthAnim(BuildingAnimSlot::Idle); diff --git a/src/Ext/BuildingType/Body.cpp b/src/Ext/BuildingType/Body.cpp index f371bb9636..83a11696f5 100644 --- a/src/Ext/BuildingType/Body.cpp +++ b/src/Ext/BuildingType/Body.cpp @@ -321,6 +321,18 @@ void BuildingTypeExt::LoadFromINIFile(CCINIClass* const pINI) prodAnim.PoweredEffect = pArtINI->ReadBool(pArtSection, "ProductionAnimPoweredEffect", prodAnim.PoweredEffect); prodAnim.PoweredSpecial = pArtINI->ReadBool(pArtSection, "ProductionAnimPoweredSpecial", prodAnim.PoweredSpecial); + this->RoofProductionAnim.Read(exArtINI, pArtSection, "RoofProductionAnim"); + this->RoofProductionAnimDamaged.Read(exArtINI, pArtSection, "RoofProductionAnimDamaged"); + this->RoofProductionAnimGarrisoned.Read(exArtINI, pArtSection, "RoofProductionAnimGarrisoned"); + this->RoofProductionAnimX.Read(exArtINI, pArtSection, "RoofProductionAnimX"); + this->RoofProductionAnimY.Read(exArtINI, pArtSection, "RoofProductionAnimY"); + this->RoofProductionAnimZAdjust.Read(exArtINI, pArtSection, "RoofProductionAnimZAdjust"); + this->RoofProductionAnimYSort.Read(exArtINI, pArtSection, "RoofProductionAnimYSort"); + this->RoofProductionAnimPowered.Read(exArtINI, pArtSection, "RoofProductionAnimPowered"); + this->RoofProductionAnimPoweredLight.Read(exArtINI, pArtSection, "RoofProductionAnimPoweredLight"); + this->RoofProductionAnimPoweredEffect.Read(exArtINI, pArtSection, "RoofProductionAnimPoweredEffect"); + this->RoofProductionAnimPoweredSpecial.Read(exArtINI, pArtSection, "RoofProductionAnimPoweredSpecial"); + // Ares tag this->SpyEffect_Custom.Read(exINI, pSection, "SpyEffect.Custom"); if (SuperWeaponTypeClass::Array.Count > 0) @@ -469,6 +481,17 @@ void BuildingTypeExt::Serialize(T& Stm) .Process(this->SetTabBySelecting) .Process(this->RevealToAll_Radius) .Process(this->DeployFireDelay) + .Process(this->RoofProductionAnim) + .Process(this->RoofProductionAnimDamaged) + .Process(this->RoofProductionAnimGarrisoned) + .Process(this->RoofProductionAnimX) + .Process(this->RoofProductionAnimY) + .Process(this->RoofProductionAnimZAdjust) + .Process(this->RoofProductionAnimYSort) + .Process(this->RoofProductionAnimPowered) + .Process(this->RoofProductionAnimPoweredLight) + .Process(this->RoofProductionAnimPoweredEffect) + .Process(this->RoofProductionAnimPoweredSpecial) // Ares 0.2 .Process(this->CloningFacility) diff --git a/src/Ext/BuildingType/Body.h b/src/Ext/BuildingType/Body.h index 26d994247c..e6dd6c92f6 100644 --- a/src/Ext/BuildingType/Body.h +++ b/src/Ext/BuildingType/Body.h @@ -129,6 +129,18 @@ class BuildingTypeExt final : public TechnoTypeExt Nullable DeployFireDelay; + Valueable RoofProductionAnim; + Valueable RoofProductionAnimDamaged; + Valueable RoofProductionAnimGarrisoned; + Nullable RoofProductionAnimX; + Nullable RoofProductionAnimY; + Nullable RoofProductionAnimZAdjust; + Nullable RoofProductionAnimYSort; + Nullable RoofProductionAnimPowered; + Nullable RoofProductionAnimPoweredLight; + Nullable RoofProductionAnimPoweredEffect; + Nullable RoofProductionAnimPoweredSpecial; + // Ares 0.2 Valueable CloningFacility; @@ -226,6 +238,18 @@ class BuildingTypeExt final : public TechnoTypeExt , RevealToAll_Radius {} , DeployFireDelay {} + , RoofProductionAnim { nullptr } + , RoofProductionAnimDamaged { nullptr } + , RoofProductionAnimGarrisoned { nullptr } + , RoofProductionAnimX {} + , RoofProductionAnimY {} + , RoofProductionAnimZAdjust {} + , RoofProductionAnimYSort {} + , RoofProductionAnimPowered { } + , RoofProductionAnimPoweredLight { } + , RoofProductionAnimPoweredEffect { } + , RoofProductionAnimPoweredSpecial { } + // Ares 0.2 , CloningFacility { false } diff --git a/src/Ext/BuildingType/Hooks.cpp b/src/Ext/BuildingType/Hooks.cpp index c79cb14363..7bdd5c6eee 100644 --- a/src/Ext/BuildingType/Hooks.cpp +++ b/src/Ext/BuildingType/Hooks.cpp @@ -434,7 +434,7 @@ DEFINE_HOOK(0x44E826, BuildingClass_GetPowerOutput_Enhancer, 0x6) const auto pOwner = pThis->Owner; auto [power, extraPower] = BuildingTypeExt::GetEnhancedPower(pThis->Type, R->EDI(), pOwner, pThis); - + if (pThis->UpgradeLevel) { for (const auto pUpgrade : pThis->Upgrades) @@ -533,6 +533,9 @@ static __forceinline bool AllowPoweredAnim(BuildingClass* pBuilding, BuildingAni } else if (anim == BuildingAnimSlot::Production || anim == BuildingAnimSlot::PreProduction) { + if (anim == BuildingAnimSlot::Production && BuildingExt::Fetch(pBuilding)->IsPlayingRoofProductionAnim) + return true; + auto const animData = pType->GetBuildingAnim(anim); if (BuildingTypeExt::IsPoweredAnimBlocked(pBuilding, animData.Powered, animData.PoweredLight, animData.PoweredEffect, animData.PoweredSpecial))