diff --git a/CREDITS.md b/CREDITS.md index ad0f227ff4..b1b4e8714e 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -671,6 +671,7 @@ This page lists all the individual contributions to the project by their author. - Return warhead - `ElectricAssault` weapons can now auto acquire allies' overpowerable defenses - Allow `AuxBuilding` and Ares' `SW.Aux/NegBuildings` to count building upgrades + - Dynamic music framework - **NaotoYuuki** - Vertical & meteor trajectory projectile prototypes - **handama**: - AI script action to `16005 Jump Back To Previous Script` @@ -812,6 +813,7 @@ This page lists all the individual contributions to the project by their author. - **Damfoos** - extensive and thorough testing - **Dmitry Volkov** - extensive and thorough testing - **Rise of the East community** - extensive playtesting of in-dev features +- **ahasasjeb** - Add music to super weapons - **11EJDE11** - Prevent mpdebug number from being drawn when visibility toggled off - **RAZER**: - Wall overlay unit sell exploit fix diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 0f1d78bbc2..d50ef6a7db 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -1310,6 +1310,26 @@ Detonate.Damage= ; integer Detonate.AtFirer=false ; boolean ``` +### Superweapon music control + +- Superweapons can now play a theme when fired and optionally stop after a configurable duration. This will replace any theme that's being played now. +- `Music.Theme` selects the soundtrack theme by its ID defined in `thememd.ini`. +- `Music.Duration` controls how many frames this theme will last. Set it to below 0 for an infinite duration. Player can still switch music manually no matter if it lasts infinitely. + - When the timer is completed and `Music.Theme` is still being played, it'll be stopped and switched to default music list. If another music is already played, it won't be switched. +- `Music.AffectsHouse` determines which houses will be affected by the music change of this superweapon. + +In `rulesmd.ini`: +```ini +[SOMESW] ; SuperWeaponType +Music.Theme=-1 ; Theme ID +Music.Duration=0 ; integer, game frames +Music.AffectsHouse=all ; Affected House Enumeration (none|owner/self|allies/ally|team|enemies/enemy|all) +``` + +```{note} +To loop the music correctly during this period, set `Repeat=yes` for the corresponding theme in `thememd.ini`. Otherwise, the track may stop at its end even if `Music.Duration` has not elapsed. +``` + ## Technos ### Aggressive attack move mission @@ -1543,6 +1563,26 @@ BuildLimitGroup.ExtraLimit.MaxCount= ; List of integers BuildLimitGroup.ExtraLimit.MaxNum=0 ; integer ``` +### Combat music + +- Music can now be changed when under attack by enemies or damaging an enemy objects. +- `CombatMusic.Theme` selects the soundtrack theme by its ID defined in `thememd.ini`. +- `CombatMusic.Duration` controls how many frames this theme will last. Set it to below 0 for an infinite duration. Player can still switch music manually no matter if it lasts infinitely. + - When the timer is completed and `CombatMusic.Theme` is still being played, it'll be stopped and switched to default music list. If another music is already played, it won't be switched. +- `CombatMusic.UnderAttack` determines if the combat music is triggered by an attack or under attack. + +In `rulesmd.ini`: +```ini +[SOMESIDE] ; Side +CombatMusic.Theme=-1 ; Theme ID +CombatMusic.Duration=0 ; integer, game frames +CombatMusic.UnderAttack=true ; boolean +``` + +```{note} +To loop the music correctly during this period, set `Repeat=yes` for the corresponding theme in `thememd.ini`. Otherwise, the track may stop at its end even if `Music.Duration` has not elapsed. +``` + ### Convert TechnoType on owner house change - You can now change a unit's type when changing ownership from human to computer or from computer to human. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index c7f490d54d..4837f8ce9f 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -583,6 +583,7 @@ HideShakeEffects=false ; boolean - Allow `RemoveMindControl` warhead to mute `MindClearedSound` (by Noble_Fish) - Introduce weight selection rules for ExtraWarheads (by Noble_Fish) - [Allow infantry to perform type conversion when deploying and undeploying](New-or-Enhanced-Logics.md#allow-infantry-to-perform-type-conversion-when-deploying-and-undeploying) (by Noble_Fish) +- Dynamic music system (by Ollerus and ahasasjeb) #### 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/House/Body.cpp b/src/Ext/House/Body.cpp index 59ddfef13b..5d83701fb1 100644 --- a/src/Ext/House/Body.cpp +++ b/src/Ext/House/Body.cpp @@ -652,6 +652,34 @@ void HouseExt::ExtData::SetForceEnemyIndex(int EnemyIndex) this->ForceEnemyIndex = EnemyIndex; } +void HouseExt::ExtData::MusicChange(int music, int duration) +{ + // restrict to current house + if (this->OwnerObject() != HouseClass::CurrentPlayer) + return; + + if (music >= 0) + { + ThemeClass::Instance.Play(music); + this->MusicTheme = music; + + if (duration > 0) + this->MusicDuration.Start(duration); + } +} + +void HouseExt::ExtData::MusicStop() +{ + // restrict to current house + if (this->OwnerObject() != HouseClass::CurrentPlayer) + return; + + if (ThemeClass::Instance.CurrentTheme == this->MusicTheme) + ThemeClass::Instance.Stop(); + + this->MusicTheme = -1; +} + void HouseExt::CalculatePowerSurplus(HouseClass* pThis) { auto const pRulesExt = RulesExt::Global(); @@ -712,6 +740,8 @@ void HouseExt::ExtData::Serialize(T& Stm) .Process(this->FreeRadar) .Process(this->ForceRadar) .Process(this->PlayerAutoRepair) + .Process(this->MusicDuration) + .Process(this->MusicTheme) ; } diff --git a/src/Ext/House/Body.h b/src/Ext/House/Body.h index 11585ed5a2..18cad3fc08 100644 --- a/src/Ext/House/Body.h +++ b/src/Ext/House/Body.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -69,6 +70,9 @@ class HouseExt bool PlayerAutoRepair; + CDTimerClass MusicDuration; + int MusicTheme; + ExtData(HouseClass* OwnerObject) : Extension(OwnerObject) , PowerPlantEnhancers {} , OwnedLimboDeliveredBuildings {} @@ -103,6 +107,8 @@ class HouseExt , FreeRadar(false) , ForceRadar(false) , PlayerAutoRepair(true) + , MusicDuration {} + , MusicTheme { -1 } { } bool OwnsLimboDeliveredBuilding(BuildingClass* pBuilding) const; @@ -115,6 +121,8 @@ class HouseExt int GetForceEnemyIndex(); void SetForceEnemyIndex(int EnemyIndex); + void MusicChange(int music, int duration); + void MusicStop(); virtual ~ExtData() = default; diff --git a/src/Ext/House/Hooks.cpp b/src/Ext/House/Hooks.cpp index dc25116588..73d2408ad9 100644 --- a/src/Ext/House/Hooks.cpp +++ b/src/Ext/House/Hooks.cpp @@ -379,11 +379,10 @@ DEFINE_HOOK(0x4F9038, HouseClass_AI_Superweapons, 0x5) return 0; const int delay = RulesExt::Global()->AISuperWeaponDelay.Get(); + auto const pExt = HouseExt::ExtMap.Find(pThis); if (delay > 0) { - auto const pExt = HouseExt::ExtMap.Find(pThis); - if (pExt->AISuperWeaponDelayTimer.HasTimeLeft()) return 0; @@ -393,6 +392,9 @@ DEFINE_HOOK(0x4F9038, HouseClass_AI_Superweapons, 0x5) if (!SessionClass::IsCampaign() || pThis->IQLevel2 >= RulesClass::Instance->SuperWeapons) pThis->AI_TryFireSW(); + if (pExt->MusicDuration.Completed()) + pExt->MusicStop(); + return 0; } diff --git a/src/Ext/SWType/Body.cpp b/src/Ext/SWType/Body.cpp index 1c63c9baa8..d7065b43c6 100644 --- a/src/Ext/SWType/Body.cpp +++ b/src/Ext/SWType/Body.cpp @@ -100,6 +100,9 @@ void SWTypeExt::ExtData::Serialize(T& Stm) .Process(this->SW_Link_RollChances) .Process(this->Message_LinkedSWAcquired) .Process(this->EVA_LinkedSWAcquired) + .Process(this->Music_Theme) + .Process(this->Music_Duration) + .Process(this->Music_AffectsHouse) ; } @@ -293,6 +296,10 @@ void SWTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) pNewSWType->Initialize(const_cast(this), OwnerObject()); pNewSWType->LoadFromINI(const_cast(this), OwnerObject(), pINI); } + + this->Music_Theme = pINI->ReadTheme(pSection, "Music.Theme", this->Music_Theme); + this->Music_Duration.Read(exINI, pSection, "Music.Duration"); + this->Music_AffectsHouse.Read(exINI, pSection, "Music.AffectsHouse"); } void SWTypeExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) diff --git a/src/Ext/SWType/Body.h b/src/Ext/SWType/Body.h index b37fae8fe0..d66aa34b3e 100644 --- a/src/Ext/SWType/Body.h +++ b/src/Ext/SWType/Body.h @@ -113,6 +113,10 @@ class SWTypeExt Valueable Message_LinkedSWAcquired; NullableIdx EVA_LinkedSWAcquired; + Valueable Music_Theme; + Valueable Music_Duration; + Valueable Music_AffectsHouse; + ExtData(SuperWeaponTypeClass* OwnerObject) : Extension(OwnerObject) , TypeID { "" } , Money_Amount { 0 } @@ -195,6 +199,9 @@ class SWTypeExt , SW_Link_RandomWeightsData {} , Message_LinkedSWAcquired {} , EVA_LinkedSWAcquired {} + , Music_Theme { -1 } + , Music_Duration { 0 } + , Music_AffectsHouse { AffectedHouse::All } { } // Ares 0.A functions diff --git a/src/Ext/SWType/FireSuperWeapon.cpp b/src/Ext/SWType/FireSuperWeapon.cpp index 442d0f6a9d..77897f342c 100644 --- a/src/Ext/SWType/FireSuperWeapon.cpp +++ b/src/Ext/SWType/FireSuperWeapon.cpp @@ -40,6 +40,9 @@ void SWTypeExt::FireSuperWeaponExt(SuperClass* pSW, const CellStruct& cell) auto& sw_ext = HouseExt::ExtMap.Find(pHouse)->SuperExts[pType->ArrayIndex]; sw_ext.ShotCount++; + if (EnumFunctions::CanTargetHouse(pTypeExt->Music_AffectsHouse, pHouse, HouseClass::CurrentPlayer)) + HouseExt::ExtMap.Find(HouseClass::CurrentPlayer)->MusicChange(pTypeExt->Music_Theme, pTypeExt->Music_Duration); + const auto pTags = &pHouse->RelatedTags; if (pTags->Count > 0) diff --git a/src/Ext/Side/Body.cpp b/src/Ext/Side/Body.cpp index ab85e7e756..82fee97b1d 100644 --- a/src/Ext/Side/Body.cpp +++ b/src/Ext/Side/Body.cpp @@ -55,6 +55,9 @@ void SideExt::ExtData::LoadFromINIFile(CCINIClass* pINI) this->SuperWeaponSidebar_TopPCX.Read(pINI, pSection, "SuperWeaponSidebar.TopPCX"); this->SuperWeaponSidebar_CenterPCX.Read(pINI, pSection, "SuperWeaponSidebar.CenterPCX"); this->SuperWeaponSidebar_BottomPCX.Read(pINI, pSection, "SuperWeaponSidebar.BottomPCX"); + this->CombatMusic_Theme = pINI->ReadTheme(pSection, "CombatMusic.Theme", this->CombatMusic_Theme); + this->CombatMusic_Duration.Read(exINI, pSection, "CombatMusic.Duration"); + this->CombatMusic_UnderAttack.Read(exINI, pSection, "CombatMusic.UnderAttack"); } // ============================= @@ -93,6 +96,9 @@ void SideExt::ExtData::Serialize(T& Stm) .Process(this->SuperWeaponSidebar_TopPCX) .Process(this->SuperWeaponSidebar_CenterPCX) .Process(this->SuperWeaponSidebar_BottomPCX) + .Process(this->CombatMusic_Theme) + .Process(this->CombatMusic_Duration) + .Process(this->CombatMusic_UnderAttack) ; } diff --git a/src/Ext/Side/Body.h b/src/Ext/Side/Body.h index 2b85a0c621..5d6dab90ad 100644 --- a/src/Ext/Side/Body.h +++ b/src/Ext/Side/Body.h @@ -44,6 +44,9 @@ class SideExt PhobosPCXFile SuperWeaponSidebar_TopPCX; PhobosPCXFile SuperWeaponSidebar_CenterPCX; PhobosPCXFile SuperWeaponSidebar_BottomPCX; + Valueable CombatMusic_Theme; + Valueable CombatMusic_Duration; + Valueable CombatMusic_UnderAttack; ExtData(SideClass* OwnerObject) : Extension(OwnerObject) , ArrayIndex { -1 } @@ -75,6 +78,9 @@ class SideExt , SuperWeaponSidebar_TopPCX {} , SuperWeaponSidebar_CenterPCX {} , SuperWeaponSidebar_BottomPCX {} + , CombatMusic_Theme { -1 } + , CombatMusic_Duration { 0 } + , CombatMusic_UnderAttack { true } { } virtual ~ExtData() = default; diff --git a/src/Ext/Techno/Hooks.ReceiveDamage.cpp b/src/Ext/Techno/Hooks.ReceiveDamage.cpp index 6ff3f2af9f..e7b683bd15 100644 --- a/src/Ext/Techno/Hooks.ReceiveDamage.cpp +++ b/src/Ext/Techno/Hooks.ReceiveDamage.cpp @@ -1,6 +1,7 @@ #include "Body.h" #include +#include #include #include #include @@ -75,6 +76,8 @@ DEFINE_HOOK(0x701900, TechnoClass_ReceiveDamage_Shield, 0x6) } // Raise Combat Alert + const auto pHouseExt = HouseExt::ExtMap.Find(pTargetHouse); + if (RulesExt::Global()->CombatAlert && damage > 1) { auto raiseCombatAlert = [&]() @@ -82,8 +85,6 @@ DEFINE_HOOK(0x701900, TechnoClass_ReceiveDamage_Shield, 0x6) if (!pTargetHouse->IsControlledByCurrentPlayer() || (RulesExt::Global()->CombatAlert_SuppressIfAllyDamage && pTargetHouse->IsAlliedWith(pSourceHouse))) return; - const auto pHouseExt = HouseExt::ExtMap.Find(pTargetHouse); - if (pHouseExt->CombatAlertTimer.HasTimeLeft() || pWHExt->CombatAlert_Suppress.Get(!pWHExt->Malicious || pWHExt->Nonprovocative)) return; @@ -129,6 +130,19 @@ DEFINE_HOOK(0x701900, TechnoClass_ReceiveDamage_Shield, 0x6) raiseCombatAlert(); } + // Combat Music + if (const auto pTargetSideExt = SideExt::ExtMap.TryFind(SideClass::Array.GetItemOrDefault(pTargetHouse->SideIndex))) + { + if (pTargetSideExt->CombatMusic_UnderAttack && !pTargetHouse->IsAlliedWith(pSourceHouse)) + pHouseExt->MusicChange(pTargetSideExt->CombatMusic_Theme, pTargetSideExt->CombatMusic_Duration); + } + + if (const auto pSourceSideExt = SideExt::ExtMap.TryFind(SideClass::Array.GetItemOrDefault(pSourceHouse->SideIndex))) + { + if (!pSourceSideExt->CombatMusic_UnderAttack && !pSourceHouse->IsAlliedWith(pTargetHouse) && !pTargetHouse->IsNeutral()) + HouseExt::ExtMap.Find(pSourceHouse)->MusicChange(pSourceSideExt->CombatMusic_Theme, pSourceSideExt->CombatMusic_Duration); + } + // Shield Receive Damage if (!args->IgnoreDefenses) {