diff --git a/CREDITS.md b/CREDITS.md index dad7ba28ca..463cdb5d44 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -391,6 +391,7 @@ This page lists all the individual contributions to the project by their author. - No turret unit turn to the target - Damage multiplier for different houses - Extended gattling rate down logic + - Draw visual effects for airburst weapons - **Ollerus** - Build limit group enhancement - Customizable rocker amplitude diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index faa4f9ae49..85eb73fbf6 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -181,6 +181,7 @@ This page describes all ingame logics that are fixed or improved in Phobos witho - Aircraft with `AirportBound=no` continue moving forward. - Now in air team members will use the 2D distance instead of the 3D distance to judge whether have reached the mission destination, so as to prevent the problem that the mission is stuck and cannot continue in some cases (such as when the jumpjet stops on the building). - Unit `Speed` setting now accepts floating-point values. Internally parsed values are clamped down to maximum of 100, multiplied by 256 and divided by 100, the result (which at this point is converted to an integer) then clamped down to maximum of 255 giving effective internal speed value range of 0 to 255, e.g leptons traveled per game frame. +- `AirburstWeapon` now supports `IsLaser`, `IsElectricBolt` (without Ares `Bolt.Color1` 、`Bolt.Color2` 、`Bolt.Color3`), `IsRadBeam`, and `AttachedParticleSystem`. - Subterranean movement now benefits from speed multipliers from all sources such as veterancy, AttachEffect etc. - Aircraft will now behave as expected according to it's `MovementZone` and `SpeedType` when moving onto different surfaces. In particular, this fixes erratic behavior when vanilla aircraft is ordered to move onto water surface and instead the movement order changes to a shore nearby. - Allowed `AuxBuilding` to count building upgrades. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 025e74b725..366f1ebb9f 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -502,6 +502,7 @@ New: - Allow infantry to use land sequences in water (by Starkku) - `` can now be used as owner for pre-placed objects on skirmish and multiplayer maps (by Starkku) - Allow customizing charge turret delays per burst on a weapon (by Starkku) +- Draw visual effects for airburst weapons (by CrimRecya) - Unit `Speed` setting now accepts floating point values (by Starkku) - Extending `Power` to all TechnoTypes (by Morton) diff --git a/src/Ext/Bullet/Body.cpp b/src/Ext/Bullet/Body.cpp index f89cee974b..021f29e66a 100644 --- a/src/Ext/Bullet/Body.cpp +++ b/src/Ext/Bullet/Body.cpp @@ -1,5 +1,6 @@ #include "Body.h" +#include #include #include #include @@ -155,6 +156,198 @@ void BulletExt::ExtData::InitializeLaserTrails() } } +static inline int SetBuildingFireAnimZAdjust(BuildingClass* pBuilding, int animY) +{ + if (pBuilding->GetOccupantCount() > 0) + return -200; + + const auto renderCoords = pBuilding->GetRenderCoords(); + const auto zAdj = (animY - renderCoords.Y) / -4; + return (zAdj >= 0) ? 0 : zAdj; +} + +// Make sure pBullet and pBullet->WeaponType is not empty before call +inline void BulletExt::SimulatedFiringAnim(BulletClass* pBullet, HouseClass* pHouse, ObjectClass* pAttach) +{ + const auto pWeapon = pBullet->WeaponType; + const auto animCounts = pWeapon->Anim.Count; + + if (animCounts <= 0) + return; + + const auto pFirer = pBullet->Owner; + const auto pAnimType = pWeapon->Anim[(animCounts % 8 == 0) // Have direction + ? (static_cast((Math::atan2(pBullet->Velocity.Y , pBullet->Velocity.X) / Math::TwoPi + 1.5) * animCounts - (animCounts / 8) + 0.5) % animCounts) // Calculate direction + : ScenarioClass::Instance->Random.RandomRanged(0 , animCounts - 1)]; // Simple random; +/* + const auto velocityRadian = Math::atan2(pBullet->Velocity.Y , pBullet->Velocity.X); + const auto ratioOfRotateAngle = velocityRadian / Math::TwoPi; + const auto correctRatioOfRotateAngle = ratioOfRotateAngle + 1.5; // Correct the Y-axis in reverse and ensure that the ratio is a positive number + const auto animIndex = correctRatioOfRotateAngle * animCounts; + const auto correctAnimIndex = animIndex - (animCounts / 8); // A multiple of 8 greater than 8 will have an additional offset + const auto trueAnimIndex = static_cast(correctAnimIndex + 0.5) % animCounts; // Round down and prevent exceeding the scope +*/ + + if (!pAnimType) + return; + + const auto pAnim = GameCreate(pAnimType, pBullet->SourceCoords); + + AnimExt::SetAnimOwnerHouseKind(pAnim, pHouse, nullptr, false, true); + AnimExt::ExtMap.Find(pAnim)->SetInvoker(pFirer, pHouse); + + if (pAttach) + { + if (pAttach->WhatAmI() == AbstractType::Building) + pAnim->ZAdjust = SetBuildingFireAnimZAdjust(static_cast(pAttach), pBullet->SourceCoords.Y); + else + pAnim->SetOwnerObject(pAttach); + } + else if (const auto pBuilding = abstract_cast(pFirer)) + { + pAnim->ZAdjust = SetBuildingFireAnimZAdjust(pBuilding, pBullet->SourceCoords.Y); + } +} + +// Make sure pBullet and pBullet->WeaponType is not empty before call +inline void BulletExt::SimulatedFiringReport(BulletClass* pBullet) +{ + const auto pWeapon = pBullet->WeaponType; + + if (pWeapon->Report.Count <= 0) + return; + + const auto pFirer = pBullet->Owner; + const auto reportIndex = pWeapon->Report[(pFirer ? pFirer->unknown_short_3C8 : ScenarioClass::Instance->Random.Random()) % pWeapon->Report.Count]; + + if (reportIndex != -1) + VocClass::PlayAt(reportIndex, pBullet->Location, nullptr); +} + +// Make sure pBullet and pBullet->WeaponType is not empty before call +inline void BulletExt::SimulatedFiringLaser(BulletClass* pBullet, HouseClass* pHouse) +{ + const auto pWeapon = pBullet->WeaponType; + + if (!pWeapon->IsLaser) + return; + + const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon); + + if (pWeapon->IsHouseColor || pWeaponExt->Laser_IsSingleColor) + { + const auto black = ColorStruct { 0, 0, 0 }; + const auto pLaser = GameCreate(pBullet->SourceCoords, pBullet->TargetCoords, ((pWeapon->IsHouseColor && pHouse) ? pHouse->LaserColor : pWeapon->LaserInnerColor), black, black, pWeapon->LaserDuration); + pLaser->IsHouseColor = true; + pLaser->Thickness = pWeaponExt->LaserThickness; + pLaser->IsSupported = (pLaser->Thickness > 3); + } + else + { + const auto pLaser = GameCreate(pBullet->SourceCoords, pBullet->TargetCoords, pWeapon->LaserInnerColor, pWeapon->LaserOuterColor, pWeapon->LaserOuterSpread, pWeapon->LaserDuration); + pLaser->IsHouseColor = false; + pLaser->Thickness = 3; + pLaser->IsSupported = false; + } +} + +// Make sure pBullet and pBullet->WeaponType is not empty before call +inline void BulletExt::SimulatedFiringElectricBolt(BulletClass* pBullet) +{ + const auto pWeapon = pBullet->WeaponType; + + if (!pWeapon->IsElectricBolt) + return; + + if (auto const pEBolt = GameCreate()) + { + pEBolt->AlternateColor = pWeapon->IsAlternateColor; + //TODO Weapon's Bolt.Color1, Bolt.Color2, Bolt.Color3(Ares) + WeaponTypeExt::BoltWeaponMap[pEBolt] = WeaponTypeExt::ExtMap.Find(pWeapon); + pEBolt->Fire(pBullet->SourceCoords, pBullet->TargetCoords, 0); + } +} + +// Make sure pBullet and pBullet->WeaponType is not empty before call +inline void BulletExt::SimulatedFiringRadBeam(BulletClass* pBullet, HouseClass* pHouse) +{ + const auto pWeapon = pBullet->WeaponType; + + if (!pWeapon->IsRadBeam) + return; + + const bool isTemporal = pWeapon->Warhead && pWeapon->Warhead->Temporal; + + if (const auto pRadBeam = RadBeam::Allocate(isTemporal ? RadBeamType::Temporal : RadBeamType::RadBeam)) + { + pRadBeam->SetCoordsSource(pBullet->SourceCoords); + pRadBeam->SetCoordsTarget(pBullet->TargetCoords); + + const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon); + + pRadBeam->Color = (pWeaponExt->Beam_IsHouseColor && pHouse) ? pHouse->LaserColor : pWeaponExt->Beam_Color.Get(isTemporal ? RulesClass::Instance->ChronoBeamColor : RulesClass::Instance->RadColor); + pRadBeam->Period = pWeaponExt->Beam_Duration; + pRadBeam->Amplitude = pWeaponExt->Beam_Amplitude; + } +} + +// Make sure pBullet and pBullet->WeaponType is not empty before call +inline void BulletExt::SimulatedFiringParticleSystem(BulletClass* pBullet, HouseClass* pHouse) +{ + if (const auto pPSType = pBullet->WeaponType->AttachedParticleSystem) + GameCreate(pPSType, pBullet->SourceCoords, pBullet->Target, pBullet->Owner, pBullet->TargetCoords, pHouse); +} + +// Make sure pBullet is not empty before call +void BulletExt::SimulatedFiringUnlimbo(BulletClass* pBullet, HouseClass* pHouse, WeaponTypeClass* pWeapon, const CoordStruct& sourceCoords, bool randomVelocity) +{ + // Weapon + pBullet->WeaponType = pWeapon; + + // Range + int projectileRange = WeaponTypeExt::ExtMap.Find(pWeapon)->ProjectileRange.Get(); + pBullet->Range = projectileRange; + + // House + BulletExt::ExtMap.Find(pBullet)->FirerHouse = pHouse; + + // Velocity + auto velocity = BulletVelocity::Empty; + + if (randomVelocity) + { + DirStruct dir; + dir.SetValue<5>(ScenarioClass::Instance->Random.RandomRanged(0, 31)); + + const auto cos_factor = -2.44921270764e-16; // cos(1.5 * Math::Pi * 1.00001) + const auto flatSpeed = cos_factor * pBullet->Speed; + + const auto radians = dir.GetRadian<32>(); + velocity = BulletVelocity { Math::cos(radians) * flatSpeed, Math::sin(radians) * flatSpeed, static_cast(-pBullet->Speed) }; + } + + // Unlimbo + pBullet->MoveTo(sourceCoords, velocity); +} + +// Make sure pBullet and pBullet->WeaponType is not empty before call +void BulletExt::SimulatedFiringEffects(BulletClass* pBullet, HouseClass* pHouse, ObjectClass* pAttach, bool firingEffect, bool visualEffect) +{ + if (firingEffect) + { + BulletExt::SimulatedFiringAnim(pBullet, pHouse, pAttach); + BulletExt::SimulatedFiringReport(pBullet); + } + + if (visualEffect) + { + BulletExt::SimulatedFiringLaser(pBullet, pHouse); + BulletExt::SimulatedFiringElectricBolt(pBullet); + BulletExt::SimulatedFiringRadBeam(pBullet, pHouse); + BulletExt::SimulatedFiringParticleSystem(pBullet, pHouse); + } +} + // ============================= // load / save diff --git a/src/Ext/Bullet/Body.h b/src/Ext/Bullet/Body.h index 700f9620a6..2bc3d6c33d 100644 --- a/src/Ext/Bullet/Body.h +++ b/src/Ext/Bullet/Body.h @@ -68,4 +68,13 @@ class BulletExt }; static ExtContainer ExtMap; + + static void SimulatedFiringUnlimbo(BulletClass* pBullet, HouseClass* pHouse, WeaponTypeClass* pWeapon, const CoordStruct& sourceCoords, bool randomVelocity); + static void SimulatedFiringEffects(BulletClass* pBullet, HouseClass* pHouse, ObjectClass* pAttach, bool firingEffect, bool visualEffect); + static inline void SimulatedFiringAnim(BulletClass* pBullet, HouseClass* pHouse, ObjectClass* pAttach); + static inline void SimulatedFiringReport(BulletClass* pBullet); + static inline void SimulatedFiringLaser(BulletClass* pBullet, HouseClass* pHouse); + static inline void SimulatedFiringElectricBolt(BulletClass* pBullet); + static inline void SimulatedFiringRadBeam(BulletClass* pBullet, HouseClass* pHouse); + static inline void SimulatedFiringParticleSystem(BulletClass* pBullet, HouseClass* pHouse); }; diff --git a/src/Ext/Bullet/Hooks.DetonateLogics.cpp b/src/Ext/Bullet/Hooks.DetonateLogics.cpp index c26893ef5a..6b53fab33c 100644 --- a/src/Ext/Bullet/Hooks.DetonateLogics.cpp +++ b/src/Ext/Bullet/Hooks.DetonateLogics.cpp @@ -437,7 +437,15 @@ DEFINE_HOOK(0x469EC0, BulletClass_Logics_AirburstWeapon, 0x6) if ((pType->Airburst || pTypeExt->Splits) && pWeapon) { auto const pSource = pThis->Owner; - auto const pOwner = pSource ? pSource->Owner : BulletExt::ExtMap.Find(pThis)->FirerHouse; + auto pOwner = pSource ? pSource->Owner : BulletExt::ExtMap.Find(pThis)->FirerHouse; + + if (!pOwner || pOwner->Defeated) + { + if (const auto pNeutral = HouseClass::FindNeutral()) + pOwner = pNeutral; + else + return SkipGameCode; + } auto& random = ScenarioClass::Instance->Random; int clusterCount = pType->Cluster; @@ -525,12 +533,11 @@ DEFINE_HOOK(0x469EC0, BulletClass_Logics_AirburstWeapon, 0x6) } } - int projectileRange = WeaponTypeExt::ExtMap.Find(pWeapon)->ProjectileRange.Get(); auto const pTypeSplits = pWeapon->Projectile; int damage = pWeapon->Damage; - if (pTypeExt->AirburstWeapon_ApplyFirepowerMult && pThis->Owner) - damage = static_cast(damage * pThis->Owner->FirepowerMultiplier * TechnoExt::ExtMap.Find(pThis->Owner)->AE.FirepowerMultiplier); + if (pTypeExt->AirburstWeapon_ApplyFirepowerMult && pSource) + damage = static_cast(damage * pSource->FirepowerMultiplier * TechnoExt::ExtMap.Find(pSource)->AE.FirepowerMultiplier); for (int i = 0; i < clusterCount; ++i) { @@ -545,7 +552,7 @@ DEFINE_HOOK(0x469EC0, BulletClass_Logics_AirburstWeapon, 0x6) int index = random.RandomRanged(0, targets.Count - 1); pTarget = targets.GetItem(index); - if (pTarget == pThis->Owner) + if (pTarget == pSource) { if (random.RandomDouble() > pTypeExt->RetargetSelf_Probability) { @@ -559,28 +566,10 @@ DEFINE_HOOK(0x469EC0, BulletClass_Logics_AirburstWeapon, 0x6) if (pTarget) { - - if (auto const pBullet = pTypeSplits->CreateBullet(pTarget, pThis->Owner, damage, pWeapon->Warhead, pWeapon->Speed, pWeapon->Bright)) + if (auto const pBullet = pTypeSplits->CreateBullet(pTarget, pSource, damage, pWeapon->Warhead, pWeapon->Speed, pWeapon->Bright)) { - BulletExt::ExtMap.Find(pBullet)->FirerHouse = pOwner; - pBullet->WeaponType = pWeapon; - pBullet->Range = projectileRange; - - DirStruct dir; - dir.SetValue<5>(random.RandomRanged(0, 31)); - - auto const radians = dir.GetRadian<32>(); - auto const sin_rad = Math::sin(radians); - auto const cos_rad = Math::cos(radians); - auto const cos_factor = -2.44921270764e-16; // cos(1.5 * Math::Pi * 1.00001) - auto const flatSpeed = cos_factor * pBullet->Speed; - - BulletVelocity velocity; - velocity.X = cos_rad * flatSpeed; - velocity.Y = sin_rad * flatSpeed; - velocity.Z = -pBullet->Speed; - - pBullet->MoveTo(pThis->Location, velocity); + BulletExt::SimulatedFiringUnlimbo(pBullet, pOwner, pWeapon, pThis->Location, true); + BulletExt::SimulatedFiringEffects(pBullet, pOwner, nullptr, false, true); } } } diff --git a/src/Ext/WeaponType/Body.cpp b/src/Ext/WeaponType/Body.cpp index d364c153a5..77a05007de 100644 --- a/src/Ext/WeaponType/Body.cpp +++ b/src/Ext/WeaponType/Body.cpp @@ -115,6 +115,12 @@ void WeaponTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->AttachEffect_CheckOnFirer.Read(exINI, pSection, "AttachEffect.CheckOnFirer"); this->AttachEffect_IgnoreFromSameSource.Read(exINI, pSection, "AttachEffect.IgnoreFromSameSource"); this->KickOutPassengers.Read(exINI, pSection, "KickOutPassengers"); + + this->Beam_Color.Read(exINI, pSection, "Beam.Color"); + this->Beam_Duration.Read(exINI, pSection, "Beam.Duration"); + this->Beam_Amplitude.Read(exINI, pSection, "Beam.Amplitude"); + this->Beam_IsHouseColor.Read(exINI, pSection, "Beam.IsHouseColor"); + this->LaserThickness.Read(exINI, pSection, "LaserThickness"); } template @@ -161,6 +167,11 @@ void WeaponTypeExt::ExtData::Serialize(T& Stm) .Process(this->AttachEffect_CheckOnFirer) .Process(this->AttachEffect_IgnoreFromSameSource) .Process(this->KickOutPassengers) + .Process(this->Beam_Color) + .Process(this->Beam_Duration) + .Process(this->Beam_Amplitude) + .Process(this->Beam_IsHouseColor) + .Process(this->LaserThickness) ; }; diff --git a/src/Ext/WeaponType/Body.h b/src/Ext/WeaponType/Body.h index 0204dbfec8..74de3874d7 100644 --- a/src/Ext/WeaponType/Body.h +++ b/src/Ext/WeaponType/Body.h @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,12 @@ class WeaponTypeExt Valueable AttachEffect_IgnoreFromSameSource; Valueable KickOutPassengers; + Nullable Beam_Color; + Valueable Beam_Duration; + Valueable Beam_Amplitude; + Valueable Beam_IsHouseColor; + Valueable LaserThickness; + ExtData(WeaponTypeClass* OwnerObject) : Extension(OwnerObject) , DiskLaser_Radius { DiskLaserClass::Radius } , ProjectileRange { Leptons(100000) } @@ -103,6 +110,11 @@ class WeaponTypeExt , AttachEffect_CheckOnFirer { false } , AttachEffect_IgnoreFromSameSource { false } , KickOutPassengers { true } + , Beam_Color {} + , Beam_Duration { 15 } + , Beam_Amplitude { 40.0 } + , Beam_IsHouseColor { false } + , LaserThickness { 3 } { } int GetBurstDelay(int burstIndex) const; @@ -138,6 +150,8 @@ class WeaponTypeExt static bool SaveGlobals(PhobosStreamWriter& Stm); static double OldRadius; + static PhobosMap BoltWeaponMap; + static const WeaponTypeExt::ExtData* BoltWeaponType; static void DetonateAt(WeaponTypeClass* pThis, AbstractClass* pTarget, TechnoClass* pOwner, HouseClass* pFiringHouse = nullptr); static void DetonateAt(WeaponTypeClass* pThis, AbstractClass* pTarget, TechnoClass* pOwner, int damage, HouseClass* pFiringHouse = nullptr); diff --git a/src/Ext/WeaponType/Hook.EBolt.cpp b/src/Ext/WeaponType/Hook.EBolt.cpp index 2db7a0de5c..51f7b3e06b 100644 --- a/src/Ext/WeaponType/Hook.EBolt.cpp +++ b/src/Ext/WeaponType/Hook.EBolt.cpp @@ -3,11 +3,8 @@ #include #include -namespace BoltTemp -{ - PhobosMap boltWeaponTypeExt; - const WeaponTypeExt::ExtData* pType = nullptr; -} +PhobosMap WeaponTypeExt::BoltWeaponMap; +const WeaponTypeExt::ExtData* WeaponTypeExt::BoltWeaponType = nullptr; DEFINE_HOOK(0x6FD494, TechnoClass_FireEBolt_SetExtMap_AfterAres, 0x7) { @@ -17,7 +14,7 @@ DEFINE_HOOK(0x6FD494, TechnoClass_FireEBolt_SetExtMap_AfterAres, 0x7) if (pWeapon) { auto const pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon); - BoltTemp::boltWeaponTypeExt[pBolt] = pWeaponExt; + WeaponTypeExt::BoltWeaponMap[pBolt] = pWeaponExt; pBolt->Lifetime = 1 << (Math::clamp(pWeaponExt->Bolt_Duration, 1, 31) - 1); } @@ -28,7 +25,7 @@ DEFINE_HOOK(0x4C2951, EBolt_DTOR, 0x5) { GET(EBolt*, pBolt, ECX); - BoltTemp::boltWeaponTypeExt.erase(pBolt); + WeaponTypeExt::BoltWeaponMap.erase(pBolt); return 0; } @@ -38,11 +35,11 @@ DEFINE_HOOK(0x4C20BC, EBolt_DrawArcs, 0xB) enum { DoLoop = 0x4C20C7, Break = 0x4C2400 }; GET_STACK(EBolt*, pBolt, 0x40); - BoltTemp::pType = BoltTemp::boltWeaponTypeExt.get_or_default(pBolt); + WeaponTypeExt::BoltWeaponType = WeaponTypeExt::BoltWeaponMap.get_or_default(pBolt); GET_STACK(int, plotIndex, STACK_OFFSET(0x408, -0x3E0)); - int arcCount = BoltTemp::pType ? BoltTemp::pType->Bolt_Arcs : 8; + int arcCount = WeaponTypeExt::BoltWeaponType ? WeaponTypeExt::BoltWeaponType->Bolt_Arcs : 8; return plotIndex < arcCount ? DoLoop : Break; } @@ -50,17 +47,17 @@ DEFINE_HOOK(0x4C20BC, EBolt_DrawArcs, 0xB) DEFINE_HOOK(0x4C24E4, Ebolt_DrawFist_Disable, 0x8) { GET_STACK(EBolt*, pBolt, 0x40); - BoltTemp::pType = BoltTemp::boltWeaponTypeExt.get_or_default(pBolt); + WeaponTypeExt::BoltWeaponType = WeaponTypeExt::BoltWeaponMap.get_or_default(pBolt); - return (BoltTemp::pType && BoltTemp::pType->Bolt_Disable1) ? 0x4C2515 : 0; + return (WeaponTypeExt::BoltWeaponType && WeaponTypeExt::BoltWeaponType->Bolt_Disable1) ? 0x4C2515 : 0; } DEFINE_HOOK(0x4C25FD, Ebolt_DrawSecond_Disable, 0xA) { - return (BoltTemp::pType && BoltTemp::pType->Bolt_Disable2) ? 0x4C262A : 0; + return (WeaponTypeExt::BoltWeaponType && WeaponTypeExt::BoltWeaponType->Bolt_Disable2) ? 0x4C262A : 0; } DEFINE_HOOK(0x4C26EE, Ebolt_DrawThird_Disable, 0x8) { - return (BoltTemp::pType && BoltTemp::pType->Bolt_Disable3) ? 0x4C2710 : 0; + return (WeaponTypeExt::BoltWeaponType && WeaponTypeExt::BoltWeaponType->Bolt_Disable3) ? 0x4C2710 : 0; }