From fdec03a43aebd5836fcececd50ac9f5c24077072 Mon Sep 17 00:00:00 2001 From: KenosisM Date: Mon, 22 Jun 2026 21:43:53 +0800 Subject: [PATCH 01/11] Port Vector effect system from KratosAI to PhobosAI --- Phobos.props | 2 +- Phobos.vcxproj | 7 +- src/Ext/Bullet/Body.cpp | 51 +- src/Ext/Bullet/Body.h | 9 + src/Ext/Bullet/Hooks.cpp | 26 +- src/Ext/Techno/Body.Update.cpp | 14 + src/New/Entity/AttachEffectClass.cpp | 59 ++ src/New/Entity/AttachEffectClass.h | 15 +- src/New/Entity/VectorState.cpp | 752 +++++++++++++++++++++++++ src/New/Entity/VectorState.h | 94 ++++ src/New/Type/AttachEffectTypeClass.cpp | 258 +++++++++ src/New/Type/AttachEffectTypeClass.h | 253 +++++++++ 12 files changed, 1526 insertions(+), 14 deletions(-) create mode 100644 src/New/Entity/VectorState.cpp create mode 100644 src/New/Entity/VectorState.h diff --git a/Phobos.props b/Phobos.props index bee83b6da5..e9d93cb259 100644 --- a/Phobos.props +++ b/Phobos.props @@ -18,7 +18,7 @@ DynamicLibrary false - v143 + v145 NotSet diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 71182c168e..35045b0e99 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -217,8 +217,6 @@ - - @@ -247,6 +245,7 @@ + @@ -357,8 +356,6 @@ - - @@ -418,4 +415,4 @@ - + \ No newline at end of file diff --git a/src/Ext/Bullet/Body.cpp b/src/Ext/Bullet/Body.cpp index ad16905d82..8806254ef6 100644 --- a/src/Ext/Bullet/Body.cpp +++ b/src/Ext/Bullet/Body.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include BulletExt::ExtContainer BulletExt::ExtMap; @@ -487,8 +489,48 @@ void BulletExt::ExtData::Serialize(T& Stm) .Process(this->ParabombFallRate) .Process(this->IsInstantDetonation) .Process(this->FirepowerMult) - - .Process(this->Trajectory) // Keep this shit at last + .Process(this->VectorType) + .Process(this->Vector.Initialized) + .Process(this->Vector.CurrentFrame) + .Process(this->Vector.DisabledTimer) + .Process(this->Vector.CurrentSpeed) + .Process(this->Vector.CurrentAngle) + .Process(this->Vector.CurrentCircleRadius) + .Process(this->Vector.CurrentCircleSpeed) + .Process(this->Vector.CurrentCircleAngle) + .Process(this->Vector.InitialOriginPos) + .Process(this->Vector.InitialLocation) + .Process(this->Vector.PrevCirclePos) + .Process(this->Vector.ArcHeight) + .Process(this->Vector.ArcRotation) + .Process(this->Vector.TargetOffset) + .Process(this->Vector.NormalRotF) + .Process(this->Vector.NormalRotL) + .Process(this->Vector.NormalRotH) + .Process(this->Vector.NormalStepF) + .Process(this->Vector.NormalStepL) + .Process(this->Vector.NormalStepH) + .Process(this->Vector.MovementFrames) + .Process(this->Vector.FacingRad) + .Process(this->Vector.TiltRad) + .Process(this->Vector.OriginOffset) + .Process(this->Vector.PrevCircleCenter) + .Process(this->Vector.OriginElapsed) + .Process(this->Vector.OriginSpeed) + .Process(this->Vector.OriginAngle) + .Process(this->Vector.OriginTargetOffset) + .Process(this->Vector.OriginCircleRadiusRuntime) + .Process(this->Vector.OriginCircleSpeedRuntime) + .Process(this->Vector.OriginCircleAngleRuntime) + .Process(this->Vector.OriginNormalRotFRuntime) + .Process(this->Vector.OriginNormalRotLRuntime) + .Process(this->Vector.OriginNormalRotHRuntime) + .Process(this->Vector.OriginNormalStepF) + .Process(this->Vector.OriginNormalStepL) + .Process(this->Vector.OriginNormalStepH) + .Process(this->Vector.OriginFacing) + .Process(this->Vector.OriginTilt) + .Process(this->Trajectory) ; } @@ -511,6 +553,11 @@ BulletExt::ExtContainer::ExtContainer() : Container("BulletClass") { } BulletExt::ExtContainer::~ExtContainer() = default; +void BulletExt::ExtData::VectorAI() +{ + VectorAI_Run(this->OwnerObject(), this->VectorType, this->Vector, this->OwnerObject() ? static_cast(this->OwnerObject())->Owner : nullptr, true); +} + // ============================= // container hooks diff --git a/src/Ext/Bullet/Body.h b/src/Ext/Bullet/Body.h index 2fd695624d..c2bbb17b63 100644 --- a/src/Ext/Bullet/Body.h +++ b/src/Ext/Bullet/Body.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include struct RadialFireStruct { @@ -38,6 +40,11 @@ class BulletExt TrajectoryPointer Trajectory; + AttachEffectTypeClass* VectorType; + VectorState Vector; + + void VectorAI(); + ExtData(BulletClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } , FirerHouse { nullptr } @@ -47,6 +54,8 @@ class BulletExt , DetonateOnInterception { true } , LaserTrails {} , Trajectory { nullptr } + , VectorType { nullptr } + , Vector {} , SnappedToTarget { false } , DamageNumberOffset { INT32_MIN } , ParabombFallRate { 0 } diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index bf55bf24ab..9c3617846b 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -15,6 +15,19 @@ DEFINE_HOOK(0x466556, BulletClass_Init, 0x6) { pExt->FirerHouse = pThis->Owner->Owner; pExt->FirepowerMult = TechnoExt::GetCurrentFirepowerMultiplier(pThis->Owner); + + // Init Vector from firing techno's AE types + auto const pTechnoExt = TechnoExt::ExtMap.Find(pThis->Owner); + for (auto const& ae : pTechnoExt->AttachedEffects) + { + auto const pAEType = ae->GetType(); + if (!pAEType) continue; + if (pAEType->HasVector() && pAEType->Vector_AffectBullets) + { + pExt->VectorType = pAEType; + break; + } + } } auto const pType = pThis->Type; @@ -44,6 +57,14 @@ DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6) BulletAITemp::ExtData = pBulletExt; BulletAITemp::TypeExtData = pBulletTypeExt; + if (pBulletExt->VectorType) + { + if (pBulletExt->Vector.CurrentFrame >= pBulletExt->VectorType->Duration) + pBulletExt->VectorType = nullptr; + else + pBulletExt->VectorAI(); + } + if (pBulletExt->InterceptedStatus & InterceptedStatus::Targeted) { if (const auto pTarget = abstract_cast(pThis->Target)) @@ -81,9 +102,8 @@ DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6) } } - //Because the laser trails will be drawn before the calculation of changing the velocity direction in each frame. - //This will cause the laser trails to be drawn in the wrong position too early, resulting in a visual appearance resembling a "bouncing". - //Let trajectories draw their own laser trails after the Trajectory's OnAI() to avoid predicting incorrect positions or pass through targets. + // VectorAI moved to BulletClass_AI_Trailer for correct engine-movement-final override + if (!pBulletExt->Trajectory && pBulletExt->LaserTrails.size()) { const CoordStruct location = pThis->GetCoords(); diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 8b8ed569bd..09d09e720a 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -1944,6 +1944,20 @@ void TechnoExt::ExtData::UpdateAttachEffects() } } + // Next AE chain + if (hasExpired && !pType->Next.empty()) + { + auto const pInvoker = attachEffect->GetInvoker(); + auto const pInvokerHouse = attachEffect->InvokerHouse; + for (auto pNextType : pType->Next) + { + if (pNextType) + AttachEffectClass::CreateAndAttach(pNextType, pThis, pThis->GetTechnoType(), this->AttachedEffects, + pInvokerHouse ? pInvokerHouse : pThis->Owner, pInvoker ? pInvoker : pThis, attachEffect->Source, AEAttachParams{}); + } + altered = true; + } + if (shouldDiscard && attachEffect->ResetIfRecreatable()) { ++it; diff --git a/src/New/Entity/AttachEffectClass.cpp b/src/New/Entity/AttachEffectClass.cpp index 15716182ac..7fbfcc8c19 100644 --- a/src/New/Entity/AttachEffectClass.cpp +++ b/src/New/Entity/AttachEffectClass.cpp @@ -17,6 +17,7 @@ AttachEffectClass::AttachEffectClass() , NeedsRecalculateStat { false } , LastDiscardCheckFrame { -1 } , LastDiscardCheckValue { false } + , Vector { } { this->HasInitialized = false; AttachEffectClass::Array.emplace_back(this); @@ -257,6 +258,7 @@ void AttachEffectClass::AI() this->CloakCheck(); this->OnlineCheck(); this->AnimCheck(); + this->VectorAI(); } void AttachEffectClass::AI_Temporal() @@ -1059,6 +1061,23 @@ void AttachEffectClass::TransferAttachedEffects(TechnoClass* pSource, TechnoClas #pragma endregion +CoordStruct AttachEffectClass::GetFLHAbsoluteCoords(CoordStruct origin, CoordStruct flh, DirStruct facing) +{ + double rad = facing.GetRadian<32>(); + double cosR = std::cos(rad); + double sinR = std::sin(rad); + return { origin.X + static_cast(flh.X * cosR + flh.Y * sinR), + origin.Y + static_cast(flh.X * sinR - flh.Y * cosR), + origin.Z + flh.Z }; +} + +void AttachEffectClass::VectorAI() +{ + if (!this->Type->Vector_AffectTechno) + return; + VectorAI_Run(this->Techno, this->Type, this->Vector, this->Invoker, false); +} + // ============================= // load / save @@ -1092,6 +1111,46 @@ bool AttachEffectClass::Serialize(T& Stm) .Process(this->LastActiveStat) .Process(this->LaserTrail) .Process(this->NeedsRecalculateStat) + .Process(this->Vector.Initialized) + .Process(this->Vector.CurrentFrame) + .Process(this->Vector.DisabledTimer) + .Process(this->Vector.CurrentSpeed) + .Process(this->Vector.CurrentAngle) + .Process(this->Vector.CurrentCircleRadius) + .Process(this->Vector.CurrentCircleSpeed) + .Process(this->Vector.CurrentCircleAngle) + .Process(this->Vector.InitialOriginPos) + .Process(this->Vector.InitialLocation) + .Process(this->Vector.PrevCirclePos) + .Process(this->Vector.ArcHeight) + .Process(this->Vector.ArcRotation) + .Process(this->Vector.TargetOffset) + .Process(this->Vector.NormalRotF) + .Process(this->Vector.NormalRotL) + .Process(this->Vector.NormalRotH) + .Process(this->Vector.NormalStepF) + .Process(this->Vector.NormalStepL) + .Process(this->Vector.NormalStepH) + .Process(this->Vector.MovementFrames) + .Process(this->Vector.FacingRad) + .Process(this->Vector.TiltRad) + .Process(this->Vector.OriginOffset) + .Process(this->Vector.PrevCircleCenter) + .Process(this->Vector.OriginElapsed) + .Process(this->Vector.OriginSpeed) + .Process(this->Vector.OriginAngle) + .Process(this->Vector.OriginTargetOffset) + .Process(this->Vector.OriginCircleRadiusRuntime) + .Process(this->Vector.OriginCircleSpeedRuntime) + .Process(this->Vector.OriginCircleAngleRuntime) + .Process(this->Vector.OriginNormalRotFRuntime) + .Process(this->Vector.OriginNormalRotLRuntime) + .Process(this->Vector.OriginNormalRotHRuntime) + .Process(this->Vector.OriginNormalStepF) + .Process(this->Vector.OriginNormalStepL) + .Process(this->Vector.OriginNormalStepH) + .Process(this->Vector.OriginFacing) + .Process(this->Vector.OriginTilt) .Success(); } diff --git a/src/New/Entity/AttachEffectClass.h b/src/New/Entity/AttachEffectClass.h index c596d6d709..50f55b8a84 100644 --- a/src/New/Entity/AttachEffectClass.h +++ b/src/New/Entity/AttachEffectClass.h @@ -1,6 +1,7 @@ #pragma once #include +#include class LaserTrailClass; @@ -22,6 +23,7 @@ class AttachEffectClass void CreateAnim(); void UpdateCumulativeAnim(); void TransferCumulativeAnim(AttachEffectClass* pSource); + void VectorAI(); bool CanShowAnim() const { @@ -58,15 +60,16 @@ class AttachEffectClass static int Detach(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo); static int DetachByGroups(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo); static void TransferAttachedEffects(TechnoClass* pSource, TechnoClass* pTarget); + static CoordStruct GetFLHAbsoluteCoords(CoordStruct origin, CoordStruct flh, DirStruct facing); + + static AttachEffectClass* CreateAndAttach(AttachEffectTypeClass* pType, TechnoClass* pTarget, TechnoTypeClass* pTargetType, std::vector>& targetAEs, HouseClass* pInvokerHouse, TechnoClass* pInvoker, + AbstractClass* pSource, AEAttachParams const& attachInfo, bool checkCumulative = true); private: void OnlineCheck(); void CloakCheck(); void AnimCheck(); - static AttachEffectClass* CreateAndAttach(AttachEffectTypeClass* pType, TechnoClass* pTarget, TechnoTypeClass* pTargetType, std::vector>& targetAEs, HouseClass* pInvokerHouse, TechnoClass* pInvoker, - AbstractClass* pSource, AEAttachParams const& attachInfo, bool checkCumulative = true); - static int DetachTypes(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo, std::vector const& types); static int RemoveAllOfType(AttachEffectTypeClass* pType, TechnoClass* pTarget, int minCount, int maxCount); @@ -81,9 +84,13 @@ class AttachEffectClass int RecreationDelay; AttachEffectTypeClass* Type; TechnoClass* Techno; + +public: HouseClass* InvokerHouse; TechnoClass* Invoker; AbstractClass* Source; + +private: AnimClass* Animation; bool IsAnimHidden; bool IsInTunnel; @@ -101,6 +108,8 @@ class AttachEffectClass bool HasCumulativeAnim; bool ShouldBeDiscarded; bool NeedsRecalculateStat; + + VectorState Vector; }; // Container for TechnoClass-specific AttachEffect fields. diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp new file mode 100644 index 0000000000..c440167842 --- /dev/null +++ b/src/New/Entity/VectorState.cpp @@ -0,0 +1,752 @@ +#include "VectorState.h" +#include "AttachEffectClass.h" +#include +#include + +static double V_Random(double min, double max) +{ + if (min >= max) return min; + return min + (max - min) * static_cast(std::rand()) / RAND_MAX; +} + +static double V_Deg2Rad(double deg) { return deg * 3.14159265358979323846 / 180.0; } +static double V_Rad2Deg(double rad) { return rad * 180.0 / 3.14159265358979323846; } + +static DirStruct V_Radians2Dir(double rad) +{ + return DirStruct(static_cast(rad * 32768.0 / 3.14159265358979323846)); +} + +static DirStruct V_Point2Dir(CoordStruct from, CoordStruct to) +{ + double dx = to.X - from.X, dy = to.Y - from.Y; + return V_Radians2Dir(std::atan2(dy, dx)); +} + +static CoordStruct V_FLHAbsoluteOffset(CoordStruct flh, DirStruct facing) +{ + return AttachEffectClass::GetFLHAbsoluteCoords(CoordStruct::Empty, flh, facing); +} + +void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorState& s, ObjectClass* pInvoker, bool isBullet) +{ + if (!pObject) + return; + + if (s.DisabledTimer > 0) + { + s.DisabledTimer--; + return; + } + + bool skipFrame = pType->Vector_TimeStep > 1 && (s.CurrentFrame % pType->Vector_TimeStep != 0); + s.CurrentFrame++; + + if (skipFrame) + return; + + s.MovementFrames++; + s.NormalRotF += s.NormalStepF; + s.NormalRotL += s.NormalStepL; + s.NormalRotH += s.NormalStepH; + + auto GetPos = [&]() { return pObject->GetCoords(); }; + + if (!s.Initialized) + { + s.Initialized = true; + s.InitialOriginPos = GetPos(); + s.InitialLocation = GetPos(); + + if (isBullet) + { + auto const pB = static_cast(pObject); + double vx = pB->Velocity.X, vy = pB->Velocity.Y; + s.FacingRad = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + } + else + { + auto const pT = static_cast(pObject); + s.FacingRad = pT->PrimaryFacing.Current().GetRadian<32>(); + } + s.TiltRad = 0.0; + s.OriginFacing = s.FacingRad; + s.OriginTilt = 0.0; + + double speed = static_cast(pType->Vector_InitialSpeed >= 0 ? pType->Vector_InitialSpeed : 0); + if (pType->Vector_RandomSpeedMin != pType->Vector_RandomSpeedMax) + speed = V_Random(pType->Vector_RandomSpeedMin, pType->Vector_RandomSpeedMax); + s.CurrentSpeed = speed; + + s.ArcHeight = static_cast(pType->Vector_ArcHeight); + if (pType->Vector_ArcRandomHeightMin != pType->Vector_ArcRandomHeightMax) + s.ArcHeight = V_Random(pType->Vector_ArcRandomHeightMin, pType->Vector_ArcRandomHeightMax); + s.ArcRotation = pType->Vector_ArcRotation; + if (pType->Vector_ArcRandomRotationMin != pType->Vector_ArcRandomRotationMax) + s.ArcRotation = V_Random(pType->Vector_ArcRandomRotationMin, pType->Vector_ArcRandomRotationMax); + + if (pType->Vector_TargetFLH.isset()) + { + CoordStruct offset = pType->Vector_TargetFLH.Get(); + if (pType->Vector_TargetOffsetFMin != pType->Vector_TargetOffsetFMax) + offset.X = pType->Vector_TargetOffsetFMin + rand() % (pType->Vector_TargetOffsetFMax - pType->Vector_TargetOffsetFMin + 1); + if (pType->Vector_TargetOffsetLMin != pType->Vector_TargetOffsetLMax) + offset.Y = pType->Vector_TargetOffsetLMin + rand() % (pType->Vector_TargetOffsetLMax - pType->Vector_TargetOffsetLMin + 1); + if (pType->Vector_TargetOffsetHMin != pType->Vector_TargetOffsetHMax) + offset.Z = pType->Vector_TargetOffsetHMin + rand() % (pType->Vector_TargetOffsetHMax - pType->Vector_TargetOffsetHMin + 1); + s.TargetOffset = offset; + } + + if (pType->Vector_NormalVector.isset()) + { + s.NormalRotF = 0.0; s.NormalRotL = 0.0; s.NormalRotH = 0.0; + auto ns = [](double ps, double m1, double M1, double m2, double M2) { + if (ps != 0.0) return ps; + if (M2 > m2 && (rand() % 2)) return m2 + (rand() / (double)RAND_MAX) * (M2 - m2); + return M1 > m1 ? m1 + (rand() / (double)RAND_MAX) * (M1 - m1) : 0.0; + }; + s.NormalStepF = ns(pType->Vector_NormalFAnglePerStep, pType->Vector_NormalFAngleRMin, pType->Vector_NormalFAngleRMax, pType->Vector_NormalFAngleRMin2, pType->Vector_NormalFAngleRMax2); + s.NormalStepL = ns(pType->Vector_NormalLAnglePerStep, pType->Vector_NormalLAngleRMin, pType->Vector_NormalLAngleRMax, pType->Vector_NormalLAngleRMin2, pType->Vector_NormalLAngleRMax2); + s.NormalStepH = ns(pType->Vector_NormalHAnglePerStep, pType->Vector_NormalHAngleRMin, pType->Vector_NormalHAngleRMax, pType->Vector_NormalHAngleRMin2, pType->Vector_NormalHAngleRMax2); + } + + if (pType->Vector_OriginNormalVector.isset()) + { + auto ns = [](double ps, double m1, double M1, double m2, double M2) { + if (ps != 0.0) return ps; + if (M2 > m2 && (rand() % 2)) return m2 + (rand() / (double)RAND_MAX) * (M2 - m2); + return M1 > m1 ? m1 + (rand() / (double)RAND_MAX) * (M1 - m1) : 0.0; + }; + s.OriginNormalStepF = ns(pType->Vector_OriginNormalFAnglePerStep, pType->Vector_OriginNormalFAngleRMin, pType->Vector_OriginNormalFAngleRMax, pType->Vector_OriginNormalFAngleRMin2, pType->Vector_OriginNormalFAngleRMax2); + s.OriginNormalStepL = ns(pType->Vector_OriginNormalLAnglePerStep, pType->Vector_OriginNormalLAngleRMin, pType->Vector_OriginNormalLAngleRMax, pType->Vector_OriginNormalLAngleRMin2, pType->Vector_OriginNormalLAngleRMax2); + s.OriginNormalStepH = ns(pType->Vector_OriginNormalHAnglePerStep, pType->Vector_OriginNormalHAngleRMin, pType->Vector_OriginNormalHAngleRMax, pType->Vector_OriginNormalHAngleRMin2, pType->Vector_OriginNormalHAngleRMax2); + } + else + { + s.OriginNormalStepF = pType->Vector_OriginNormalFAnglePerStep; + s.OriginNormalStepL = pType->Vector_OriginNormalLAnglePerStep; + s.OriginNormalStepH = pType->Vector_OriginNormalHAnglePerStep; + } + } + + // === Dynamic facing === + double effectiveFacing = s.FacingRad + V_Deg2Rad(s.NormalRotH); + double effectiveTilt = s.TiltRad + V_Deg2Rad(s.NormalRotL); + DirStruct mainFacingDir = V_Radians2Dir(effectiveFacing); + + if (pType->Vector_OriginIsOnWorld) + { + mainFacingDir = DirStruct{}; + effectiveFacing = 0.0; + effectiveTilt = 0.0; + } + + bool hasNormal = pType->Vector_NormalVector.isset(); + if (!pType->Vector_OriginNoUpdate && !hasNormal && !pType->Vector_OriginIsOnWorld) + { + switch (pType->Vector_Origin) + { + case VectorOrigin::Source: + if (pInvoker) + { + mainFacingDir = V_Point2Dir(pInvoker->GetCoords(), GetPos()); + effectiveFacing = mainFacingDir.GetRadian<32>(); + if (pType->Vector_AllowedTilt) + { + double dx = GetPos().X - pInvoker->GetCoords().X; + double dy = GetPos().Y - pInvoker->GetCoords().Y; + double dz = GetPos().Z - pInvoker->GetCoords().Z; + double lenXY = std::sqrt(dx * dx + dy * dy); + effectiveTilt = (lenXY > 1e-6) ? std::atan2(dz, lenXY) : 0.0; + } + } + break; + case VectorOrigin::Target: + { + CoordStruct targetPos; + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) + targetPos = pB->Target->GetCoords(); + else + break; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) + targetPos = pT->Target->GetCoords(); + else + break; + } + mainFacingDir = V_Point2Dir(targetPos, GetPos()); + effectiveFacing = mainFacingDir.GetRadian<32>(); + if (pType->Vector_AllowedTilt) + { + double dx = GetPos().X - targetPos.X; + double dy = GetPos().Y - targetPos.Y; + double dz = GetPos().Z - targetPos.Z; + double lenXY = std::sqrt(dx * dx + dy * dy); + effectiveTilt = (lenXY > 1e-6) ? std::atan2(dz, lenXY) : 0.0; + } + break; + } + case VectorOrigin::Self: + if (isBullet) + { + double vx = static_cast(pObject)->Velocity.X; + double vy = static_cast(pObject)->Velocity.Y; + effectiveFacing = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + mainFacingDir = V_Radians2Dir(effectiveFacing); + } + else + { + auto const pT = static_cast(pObject); + mainFacingDir = pType->Vector_OriginIsOnBody + ? pT->PrimaryFacing.Current() + : pT->TurretFacing(); + effectiveFacing = mainFacingDir.GetRadian<32>(); + } + break; + case VectorOrigin::Launcher: + if (pInvoker) + { + auto const pLT = static_cast(pInvoker); + mainFacingDir = pType->Vector_OriginIsOnBody + ? pLT->PrimaryFacing.Current() + : pLT->TurretFacing(); + effectiveFacing = mainFacingDir.GetRadian<32>(); + } + break; + } + } + + // === Origin coordinate (per-frame tracking) === + CoordStruct originPos = GetPos(); + + switch (pType->Vector_Origin) + { + case VectorOrigin::Target: + if (pType->Vector_OriginNoUpdate) + originPos = s.InitialOriginPos; + else if (isBullet) + { + auto const pB = static_cast(pObject); + originPos = pB->Target ? pB->Target->GetCoords() : pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + originPos = pT->Target ? pT->Target->GetCoords() : originPos; + } + break; + case VectorOrigin::Launcher: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Source: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Self: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : GetPos(); + break; + } + + if (pType->Vector_OriginFLH.isset() && pType->Vector_Origin != VectorOrigin::Self) + originPos = AttachEffectClass::GetFLHAbsoluteCoords(originPos, pType->Vector_OriginFLH, mainFacingDir); + + CoordStruct currentPos = GetPos(); + + // ================================================================ + // Circle mode + // ================================================================ + bool hasCircle = pType->Vector_CircleRadius > 0 || pType->Vector_CircleSpeed != 0 || pType->Vector_CircleAnglePerStep > 0.0 + || (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + || (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin); + + if (hasCircle) + { + double calcRadius = static_cast(pType->Vector_CircleRadius); + if (calcRadius <= 0.0) + { + double tdx = currentPos.X - originPos.X; + double tdy = currentPos.Y - originPos.Y; + calcRadius = std::sqrt(tdx * tdx + tdy * tdy); + } + + if (s.MovementFrames == 1) + { + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleSpeed); + s.CurrentCircleRadius = calcRadius; + if (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + s.CurrentCircleRadius = static_cast(rand() % (pType->Vector_CircleRandomRadiusMax - pType->Vector_CircleRandomRadiusMin + 1) + pType->Vector_CircleRandomRadiusMin); + } + s.CurrentCircleSpeed += pType->Vector_CircleSpeedAcceleration; + if (pType->Vector_CircleMaxSpeed != 0 && s.CurrentCircleSpeed > pType->Vector_CircleMaxSpeed) + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleMaxSpeed); + if (pType->Vector_CircleMinSpeed != 0 && s.CurrentCircleSpeed < pType->Vector_CircleMinSpeed) + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleMinSpeed); + + if (s.MovementFrames == 1) + { + s.CurrentCircleAngle = pType->Vector_CircleAnglePerStep; + if (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin) + s.CurrentCircleAngle = V_Random(pType->Vector_CircleRandomAngleMin, pType->Vector_CircleRandomAngleMax); + } + s.CurrentCircleAngle += pType->Vector_CircleAngleAcceleration; + if (pType->Vector_CircleMaxAngle != 0 && s.CurrentCircleAngle > pType->Vector_CircleMaxAngle) + s.CurrentCircleAngle = pType->Vector_CircleMaxAngle; + if (pType->Vector_CircleMinAngle != 0 && s.CurrentCircleAngle < pType->Vector_CircleMinAngle) + s.CurrentCircleAngle = pType->Vector_CircleMinAngle; + + double speed = s.CurrentCircleSpeed; + double angleStep = s.CurrentCircleAngle; + if (speed <= 0.0 && angleStep > 0.0) + speed = calcRadius * V_Deg2Rad(angleStep); + else if (angleStep <= 0.0 && speed > 0.0) + angleStep = V_Rad2Deg(speed / calcRadius); + + CoordStruct circleCenter = originPos; + if (pType->Vector_CircleOrigin.isset()) + { + if (pType->Vector_AllowOriginTilt) + circleCenter = AttachEffectClass::GetFLHAbsoluteCoords(originPos, pType->Vector_CircleOrigin, mainFacingDir); + else + circleCenter = { originPos.X + pType->Vector_CircleOrigin.Get().X, originPos.Y + pType->Vector_CircleOrigin.Get().Y, originPos.Z + pType->Vector_CircleOrigin.Get().Z }; + } + + bool hasOriginSub = pType->Vector_OriginMoveTo.isset() + || pType->Vector_OriginTargetFLH.isset() + || pType->Vector_OriginCircleRadius >= 0 || pType->Vector_OriginCircleSpeed != 0 || pType->Vector_OriginCircleAnglePerStep != 0; + + if (hasOriginSub) + { + CoordStruct baseCenter = originPos; + + if (pType->Vector_OriginOrigin != VectorOrigin::Self) + { + switch (pType->Vector_OriginOrigin) + { + case VectorOrigin::Launcher: + if (pInvoker) baseCenter = pInvoker->GetCoords(); + break; + case VectorOrigin::Target: + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) baseCenter = pB->Target->GetCoords(); + else if (pB->Owner && pB->Owner->Target) baseCenter = pB->Owner->Target->GetCoords(); + else baseCenter = pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) baseCenter = pT->Target->GetCoords(); + else + { + auto const pFoot = abstract_cast(pT); + if (pFoot && pFoot->Destination) baseCenter = pFoot->Destination->GetCoords(); + } + } + break; + case VectorOrigin::Source: + if (pInvoker) baseCenter = pInvoker->GetCoords(); + break; + } + } + else if (pType->Vector_OriginOriginFLH.isset()) + { + baseCenter.X += pType->Vector_OriginOriginFLH.Get().X; + baseCenter.Y += pType->Vector_OriginOriginFLH.Get().Y; + baseCenter.Z += pType->Vector_OriginOriginFLH.Get().Z; + } + + if (s.MovementFrames == 1) + { + s.OriginOffset = { circleCenter.X - baseCenter.X, circleCenter.Y - baseCenter.Y, circleCenter.Z - baseCenter.Z }; + s.OriginCircleRadiusRuntime = static_cast(pType->Vector_OriginCircleRadius); + s.OriginCircleSpeedRuntime = static_cast(pType->Vector_OriginCircleSpeed); + s.OriginCircleAngleRuntime = 0.0; + s.OriginTargetOffset = CoordStruct::Empty; + + if (pType->Vector_OriginNormalVector.isset()) + { + CoordStruct nv = pType->Vector_OriginNormalVector.Get(); + double len = std::sqrt(static_cast(nv.X * nv.X + nv.Y * nv.Y)); + s.OriginFacing = len > 1e-6 ? std::atan2(static_cast(nv.Y), static_cast(nv.X)) : 0; + s.OriginTilt = len > 1e-6 ? std::atan2(static_cast(nv.Z), len) : (nv.Z > 0 ? 3.14159265358979323846 / 2.0 : -3.14159265358979323846 / 2.0); + } + } + + s.OriginNormalRotFRuntime += s.OriginNormalStepF; + s.OriginNormalRotLRuntime += s.OriginNormalStepL; + s.OriginNormalRotHRuntime += s.OriginNormalStepH; + + double oFacing = s.OriginFacing + V_Deg2Rad(s.OriginNormalRotHRuntime); + double oTilt = s.OriginTilt + V_Deg2Rad(s.OriginNormalRotLRuntime); + + CoordStruct originCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + CoordStruct disp{ 0, 0, 0 }; + + if (pType->Vector_OriginMoveTo.isset()) + { + s.OriginAngle += pType->Vector_OriginCircleAnglePerStep; + CoordStruct mto = pType->Vector_OriginMoveTo.Get(); + if (pType->Vector_OriginGrowRate.isset()) + { + mto.X += pType->Vector_OriginGrowRate.Get().X * s.OriginElapsed; + mto.Y += pType->Vector_OriginGrowRate.Get().Y * s.OriginElapsed; + mto.Z += pType->Vector_OriginGrowRate.Get().Z * s.OriginElapsed; + } + disp = V_FLHAbsoluteOffset(mto, V_Radians2Dir(oFacing + V_Deg2Rad(s.OriginAngle))); + } + else if (pType->Vector_OriginTargetFLH.isset()) + { + if (s.OriginElapsed == 0) + s.OriginSpeed = pType->Vector_OriginInitialSpeed >= 0 ? pType->Vector_OriginInitialSpeed : 40.0; + + CoordStruct targetWorld = AttachEffectClass::GetFLHAbsoluteCoords(baseCenter, pType->Vector_OriginTargetFLH.Get() + s.OriginTargetOffset, V_Radians2Dir(oFacing)); + int dx = targetWorld.X - originCenter.X, dy = targetWorld.Y - originCenter.Y, dz = targetWorld.Z - originCenter.Z; + double dist = std::sqrt(static_cast(dx * dx + dy * dy + dz * dz)); + if (dist >= 1.0) + { + double sv = s.OriginSpeed / dist; + disp = { static_cast(dx * sv), static_cast(dy * sv), static_cast(dz * sv) }; + } + } + else + { + s.OriginCircleRadiusRuntime += pType->Vector_OriginCircleRadiusGrow; + double tr = s.OriginCircleRadiusRuntime; + if (pType->Vector_OriginCircleMaxRadius > 0 && tr > pType->Vector_OriginCircleMaxRadius) tr = pType->Vector_OriginCircleMaxRadius; + if (pType->Vector_OriginCircleMinRadius > 0 && tr < pType->Vector_OriginCircleMinRadius) tr = pType->Vector_OriginCircleMinRadius; + double stepO = pType->Vector_OriginCircleAnglePerStep; + if (pType->Vector_OriginCircleSpeed != 0 && tr > 0) + stepO = V_Rad2Deg(pType->Vector_OriginCircleSpeed / tr); + s.OriginCircleAngleRuntime += stepO; + double r = V_Deg2Rad(stepO); + double ca = std::cos(r), sa = std::sin(r); + double dxO = static_cast(s.OriginOffset.X); + double dyO = static_cast(s.OriginOffset.Y); + double dzO = static_cast(s.OriginOffset.Z); + double cf = std::cos(oFacing), sf = std::sin(oFacing), ct = std::cos(oTilt), st = std::sin(oTilt); + double dL = dxO * (-sf) + dyO * cf; + double dH = dxO * (-cf * st) + dyO * (-sf * st) + dzO * ct; + double cd = std::sqrt(dL * dL + dH * dH); + if (cd < 1.0 && tr > 0) { dL = tr; dH = 0; cd = tr; } + else if (cd < 1.0) cd = 1.0; + double rL = (dL / cd * tr * ca) - (dH / cd * tr * sa); + double rH = (dL / cd * tr * sa) + (dH / cd * tr * ca); + CoordStruct newOffset; + newOffset.X = static_cast(rL * (-sf) + rH * (-cf * st)); + newOffset.Y = static_cast(rL * cf + rH * (-sf * st)); + newOffset.Z = static_cast(rH * ct); + disp.X = newOffset.X - s.OriginOffset.X; + disp.Y = newOffset.Y - s.OriginOffset.Y; + disp.Z = newOffset.Z - s.OriginOffset.Z; + } + + s.OriginOffset.X += disp.X; s.OriginOffset.Y += disp.Y; s.OriginOffset.Z += disp.Z; + circleCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + s.OriginElapsed++; + } + + CoordStruct centerDelta{ 0, 0, 0 }; + bool useCenterTracking = false; + if (s.PrevCircleCenter.X || s.PrevCircleCenter.Y || s.PrevCircleCenter.Z) + { + centerDelta.X = circleCenter.X - s.PrevCircleCenter.X; + centerDelta.Y = circleCenter.Y - s.PrevCircleCenter.Y; + centerDelta.Z = circleCenter.Z - s.PrevCircleCenter.Z; + useCenterTracking = (pType->Vector_OriginCircleRadius >= 0 || pType->Vector_OriginCircleSpeed != 0 || pType->Vector_OriginCircleAnglePerStep != 0); + } + s.PrevCircleCenter = circleCenter; + + CoordStruct trackPos = currentPos; + if (useCenterTracking) + { + trackPos.X += centerDelta.X; trackPos.Y += centerDelta.Y; trackPos.Z += centerDelta.Z; + } + + double dx = static_cast(trackPos.X - circleCenter.X); + double dy = static_cast(trackPos.Y - circleCenter.Y); + double dz = static_cast(trackPos.Z - circleCenter.Z); + double currentDist; + bool useTiltPlane = hasNormal || (pType->Vector_AllowedTilt && effectiveTilt != 0.0); + + if (useTiltPlane) + { + double cosF = std::cos(effectiveFacing), sinF = std::sin(effectiveFacing); + double cosT = std::cos(effectiveTilt), sinT = std::sin(effectiveTilt); + double dL = dx * (-sinF) + dy * cosF; + double dH = dx * (-cosF * sinT) + dy * (-sinF * sinT) + dz * cosT; + currentDist = std::sqrt(dL * dL + dH * dH); + } + else + { + currentDist = std::sqrt(dx * dx + dy * dy); + } + bool startAtCenter = currentDist < 1.0; + if (currentDist < 1.0) currentDist = 1.0; + + s.CurrentCircleRadius += pType->Vector_CircleRadiusGrow; + double targetRadius = s.CurrentCircleRadius; + if (pType->Vector_CircleMaxRadius > 0 && targetRadius > pType->Vector_CircleMaxRadius) + targetRadius = static_cast(pType->Vector_CircleMaxRadius); + if (pType->Vector_CircleMinRadius > 0 && targetRadius < pType->Vector_CircleMinRadius) + targetRadius = static_cast(pType->Vector_CircleMinRadius); + + if (startAtCenter) + { + dx = targetRadius; dy = 0.0; currentDist = targetRadius; + } + + double rad = V_Deg2Rad(angleStep); + double cosA = std::cos(rad), sinA = std::sin(rad); + CoordStruct moveDisp{ 0, 0, 0 }; + + if (useTiltPlane) + { + double cosF = std::cos(effectiveFacing), sinF = std::sin(effectiveFacing); + double cosT = std::cos(effectiveTilt), sinT = std::sin(effectiveTilt); + double dL = dx * (-sinF) + dy * cosF; + double dH = dx * (-cosF * sinT) + dy * (-sinF * sinT) + dz * cosT; + double curDist = std::sqrt(dL * dL + dH * dH); + if (curDist < 1.0) curDist = 1.0; + double ndL = dL / curDist * targetRadius; + double ndH = dH / curDist * targetRadius; + double rL = ndL * cosA - ndH * sinA; + double rH = ndL * sinA + ndH * cosA; + moveDisp.X = circleCenter.X + static_cast(rL * (-sinF) + rH * (-cosF * sinT)) - currentPos.X; + moveDisp.Y = circleCenter.Y + static_cast(rL * cosF + rH * (-sinF * sinT)) - currentPos.Y; + moveDisp.Z = circleCenter.Z + static_cast(rH * cosT) - currentPos.Z; + } + else + { + double ndx = dx / currentDist * targetRadius; + double ndy = dy / currentDist * targetRadius; + double rx = ndx * cosA - ndy * sinA; + double ry = ndx * sinA + ndy * cosA; + moveDisp.X = circleCenter.X + static_cast(rx) - currentPos.X; + moveDisp.Y = circleCenter.Y + static_cast(ry) - currentPos.Y; + moveDisp.Z = circleCenter.Z - currentPos.Z; + } + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = moveDisp.X; + pB->Velocity.Y = moveDisp.Y; + pB->Velocity.Z = moveDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + + if (pType->Vector_CircleEndOnMaxRadius && pType->Vector_CircleMaxRadius > 0 && s.CurrentCircleRadius >= pType->Vector_CircleMaxRadius) + return; + if (pType->Vector_CircleEndOnMinRadius && pType->Vector_CircleMinRadius > 0 && s.CurrentCircleRadius <= pType->Vector_CircleMinRadius) + return; + + return; +} + +// ================================================================ +// MoveTo mode +// ================================================================ + if (static_cast(pType->Vector_MoveTo) != CoordStruct::Empty) + { + DirStruct moveDir = mainFacingDir; + if (pType->Vector_AnglePerStep != 0.0) + { + if (s.MovementFrames == 1) + s.CurrentAngle = 0.0; + s.CurrentAngle += pType->Vector_AnglePerStep; + moveDir = V_Radians2Dir(mainFacingDir.GetRadian<32>() + V_Deg2Rad(s.CurrentAngle)); + } + + CoordStruct grow{ static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().X * s.MovementFrames : 0), + static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().Y * s.MovementFrames : 0), + static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().Z * s.MovementFrames : 0) }; + CoordStruct mt = pType->Vector_MoveTo; + CoordStruct moveFlh = { mt.X + grow.X, mt.Y + grow.Y, mt.Z + grow.Z }; + + CoordStruct moveDisp = V_FLHAbsoluteOffset(moveFlh, moveDir); + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = moveDisp.X; pB->Velocity.Y = moveDisp.Y; pB->Velocity.Z = moveDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + return; + } + + // ================================================================ + // TargetFLH modes: ReachTarget / Speed + // ================================================================ + if (!pType->Vector_TargetFLH.isset()) + { + if (!isBullet && pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + return; + } + + CoordStruct frameTargetFlh = { pType->Vector_TargetFLH.Get().X + s.TargetOffset.X, + pType->Vector_TargetFLH.Get().Y + s.TargetOffset.Y, + pType->Vector_TargetFLH.Get().Z + s.TargetOffset.Z }; + CoordStruct frameTarget; + + switch (pType->Vector_Origin) + { + case VectorOrigin::Launcher: + if (pInvoker) + frameTarget = TechnoExt::GetFLHAbsoluteCoords(static_cast(pInvoker), frameTargetFlh, !pType->Vector_OriginIsOnBody); + else + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, mainFacingDir); + break; + case VectorOrigin::Self: + if (pType->Vector_OriginIsOnWorld) + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, DirStruct{}); + else if (isBullet) + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(currentPos, frameTargetFlh, mainFacingDir); + else + frameTarget = TechnoExt::GetFLHAbsoluteCoords(static_cast(pObject), frameTargetFlh, !pType->Vector_OriginIsOnBody); + break; + default: + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, mainFacingDir); + break; + } + + CoordStruct dirVec = { frameTarget.X - currentPos.X, frameTarget.Y - currentPos.Y, frameTarget.Z - currentPos.Z }; + double dirLen = std::sqrt(static_cast(dirVec.X * dirVec.X + dirVec.Y * dirVec.Y + dirVec.Z * dirVec.Z)); + CoordStruct resultDisp{ 0, 0, 0 }; + + if (pType->Vector_ReachTarget) + { + int totalDuration = isBullet ? 60 : 60; + int effectiveDuration = totalDuration - pType->Vector_DisabledFrames; + if (effectiveDuration < 1) effectiveDuration = 1; + int remainingFrames = effectiveDuration - s.MovementFrames + 1; + + if (pType->Vector_ReachTargetEarlyEnd > 0 && remainingFrames <= pType->Vector_ReachTargetEarlyEnd) + { + s.DisabledTimer = pType->Vector_ReachTargetEarlyEnd; + return; + } + + if (remainingFrames <= 0) + { + resultDisp.X = frameTarget.X - currentPos.X; + resultDisp.Y = frameTarget.Y - currentPos.Y; + resultDisp.Z = frameTarget.Z - currentPos.Z; + } + else if (dirLen > 1e-6) + { + double adjustedSpeed = dirLen / remainingFrames; + resultDisp.X = static_cast(dirVec.X / dirLen * adjustedSpeed); + resultDisp.Y = static_cast(dirVec.Y / dirLen * adjustedSpeed); + resultDisp.Z = static_cast(dirVec.Z / dirLen * adjustedSpeed); + + if (s.ArcHeight != 0) + { + double t = static_cast(s.MovementFrames - 1) / effectiveDuration; + double tNext = static_cast(s.MovementFrames) / effectiveDuration; + double peakScale = pType->Vector_ArcPeakPercent > 0 ? 1.0 / (pType->Vector_ArcPeakPercent * (1.0 - pType->Vector_ArcPeakPercent)) : 4.0; + double delta = peakScale * s.ArcHeight * (tNext * (1.0 - tNext) - t * (1.0 - t)); + + if (s.ArcRotation == 0.0) + { + resultDisp.Z += static_cast(delta); + } + else + { + double ax = frameTarget.X - s.InitialLocation.X; + double ay = frameTarget.Y - s.InitialLocation.Y; + double az = frameTarget.Z - s.InitialLocation.Z; + double aLen = std::sqrt(ax * ax + ay * ay + az * az); + if (aLen > 1e-6) + { + double dnx = ax / aLen, dny = ay / aLen, dnz = az / aLen; + double upDotD = dnz; + double px = -dnx * upDotD, py = -dny * upDotD, pz = 1.0 - dnz * upDotD; + double pLen = std::sqrt(px * px + py * py + pz * pz); + if (pLen < 1e-6) { px = 1.0 - dnx * dnx; py = -dny * dnx; pz = -dnz * dnx; pLen = std::sqrt(px * px + py * py + pz * pz); } + double pnx = px / pLen, pny = py / pLen, pnz = pz / pLen; + double arcRad = V_Deg2Rad(s.ArcRotation); + double c = std::cos(arcRad), s_arc = std::sin(arcRad); + double rx = pnx * c + (dny * pnz - dnz * pny) * s_arc; + double ry = pny * c + (dnz * pnx - dnx * pnz) * s_arc; + double rz = pnz * c + (dnx * pny - dny * pnx) * s_arc; + resultDisp.X += static_cast(rx * delta); + resultDisp.Y += static_cast(ry * delta); + resultDisp.Z += static_cast(rz * delta); + } + else + { + resultDisp.Z += static_cast(delta); + } + } + } + } + } + else if (dirLen > 1e-6) + { + double spd = s.CurrentSpeed; + if (pType->Vector_Acceleration != 0) spd += pType->Vector_Acceleration * s.CurrentFrame; + if (pType->Vector_MinSpeed >= 0 && spd < pType->Vector_MinSpeed) spd = pType->Vector_MinSpeed; + if (pType->Vector_MaxSpeed >= 0 && spd > pType->Vector_MaxSpeed) spd = pType->Vector_MaxSpeed; + resultDisp.X = static_cast(dirVec.X / dirLen * spd); + resultDisp.Y = static_cast(dirVec.Y / dirLen * spd); + resultDisp.Z = static_cast(dirVec.Z / dirLen * spd); + } + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = resultDisp.X; + pB->Velocity.Y = resultDisp.Y; + pB->Velocity.Z = resultDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + resultDisp.X, currentPos.Y + resultDisp.Y, currentPos.Z + resultDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + + if (!isBullet && pType->Vector_SyncFacing && (resultDisp.X != 0 || resultDisp.Y != 0)) + { + double ang = std::atan2(static_cast(resultDisp.Y), static_cast(resultDisp.X)); + auto dir = DirStruct(static_cast(ang * 32768.0 / 3.14159265358979323846)); + static_cast(pObject)->PrimaryFacing.SetDesired(dir); + } + + if (pType->Vector_AllowFallingDestroy && pType->Vector_FallingDestroyHeight > 0) + { + if (s.InitialLocation.Z - GetPos().Z > pType->Vector_FallingDestroyHeight) + return; + } +} diff --git a/src/New/Entity/VectorState.h b/src/New/Entity/VectorState.h new file mode 100644 index 0000000000..cc08bed4fd --- /dev/null +++ b/src/New/Entity/VectorState.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +#include + +struct VectorState +{ + bool Initialized; + int CurrentFrame; + int DisabledTimer; + double CurrentSpeed; + double CurrentAngle; + double CurrentCircleRadius; + double CurrentCircleSpeed; + double CurrentCircleAngle; + CoordStruct InitialOriginPos; + CoordStruct InitialLocation; + CoordStruct PrevCirclePos; + double ArcHeight; + double ArcRotation; + CoordStruct TargetOffset; + double NormalRotF; + double NormalRotL; + double NormalRotH; + double NormalStepF; + double NormalStepL; + double NormalStepH; + int MovementFrames; + double FacingRad; + double TiltRad; + CoordStruct OriginOffset; + CoordStruct PrevCircleCenter; + int OriginElapsed; + double OriginSpeed; + double OriginAngle; + CoordStruct OriginTargetOffset; + double OriginCircleRadiusRuntime; + double OriginCircleSpeedRuntime; + double OriginCircleAngleRuntime; + double OriginNormalRotFRuntime; + double OriginNormalRotLRuntime; + double OriginNormalRotHRuntime; + double OriginNormalStepF; + double OriginNormalStepL; + double OriginNormalStepH; + double OriginFacing; + double OriginTilt; + + VectorState() + : Initialized { false } + , CurrentFrame { 0 } + , DisabledTimer { 0 } + , CurrentSpeed { 0.0 } + , CurrentAngle { 0.0 } + , CurrentCircleRadius { 0.0 } + , CurrentCircleSpeed { 0.0 } + , CurrentCircleAngle { 0.0 } + , InitialOriginPos { CoordStruct::Empty } + , InitialLocation { CoordStruct::Empty } + , PrevCirclePos { CoordStruct::Empty } + , ArcHeight { 0.0 } + , ArcRotation { 0.0 } + , TargetOffset { CoordStruct::Empty } + , NormalRotF { 0.0 } + , NormalRotL { 0.0 } + , NormalRotH { 0.0 } + , NormalStepF { 0.0 } + , NormalStepL { 0.0 } + , NormalStepH { 0.0 } + , MovementFrames { 0 } + , FacingRad { 0.0 } + , TiltRad { 0.0 } + , OriginOffset { CoordStruct::Empty } + , PrevCircleCenter { CoordStruct::Empty } + , OriginElapsed { 0 } + , OriginSpeed { 0.0 } + , OriginAngle { 0.0 } + , OriginTargetOffset { CoordStruct::Empty } + , OriginCircleRadiusRuntime { 0.0 } + , OriginCircleSpeedRuntime { 0.0 } + , OriginCircleAngleRuntime { 0.0 } + , OriginNormalRotFRuntime { 0.0 } + , OriginNormalRotLRuntime { 0.0 } + , OriginNormalRotHRuntime { 0.0 } + , OriginNormalStepF { 0.0 } + , OriginNormalStepL { 0.0 } + , OriginNormalStepH { 0.0 } + , OriginFacing { 0.0 } + , OriginTilt { 0.0 } + { } +}; + +void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorState& state, ObjectClass* pInvoker, bool isBullet); diff --git a/src/New/Type/AttachEffectTypeClass.cpp b/src/New/Type/AttachEffectTypeClass.cpp index 941cfea4ad..76b7d8113a 100644 --- a/src/New/Type/AttachEffectTypeClass.cpp +++ b/src/New/Type/AttachEffectTypeClass.cpp @@ -126,6 +126,7 @@ void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) this->ExpireWeapon_TriggerOn.Read(exINI, pSection, "ExpireWeapon.TriggerOn"); this->ExpireWeapon_CumulativeOnlyOnce.Read(exINI, pSection, "ExpireWeapon.CumulativeOnlyOnce"); this->ExpireWeapon_UseInvokerAsOwner.Read(exINI, pSection, "ExpireWeapon.UseInvokerAsOwner"); + this->Next.Read(exINI, pSection, "Next"); this->Tint_Color.Read(exINI, pSection, "Tint.Color"); this->Tint_Intensity.Read(exINI, pSection, "Tint.Intensity"); @@ -179,6 +180,124 @@ void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) this->Unkillable.Read(exINI, pSection, "Unkillable"); this->LaserTrail_Type.Read(exINI, pSection, "LaserTrail.Type"); + // Vector + this->Vector_TimeStep.Read(exINI, pSection, "Vector.TimeStep"); + if (this->Vector_TimeStep < 1) + this->Vector_TimeStep = 1; + this->Vector_DisabledFrames.Read(exINI, pSection, "Vector.DisabledFrames"); + this->Vector_SyncFacing.Read(exINI, pSection, "Vector.SyncFacing"); + this->Vector_OriginIsOnWorld.Read(exINI, pSection, "Vector.OriginIsOnWorld"); + this->Vector_OriginIsOnBody.Read(exINI, pSection, "Vector.OriginIsOnBody"); + this->Vector_Origin.Read(exINI, pSection, "Vector.Origin"); + this->Vector_OriginFLH.Read(exINI, pSection, "Vector.OriginFLH"); + this->Vector_OriginNoUpdate.Read(exINI, pSection, "Vector.OriginNoUpdate"); + this->Vector_Force.Read(exINI, pSection, "Vector.Force"); + this->Vector_Freeze.Read(exINI, pSection, "Vector.Freeze"); + this->Vector_AllowedTilt.Read(exINI, pSection, "Vector.AllowedTilt"); + this->Vector_NormalVector.Read(exINI, pSection, "Vector.NormalVector"); + this->Vector_NormalRandomF.Read(exINI, pSection, "Vector.NormalRandomF"); + this->Vector_NormalRandomL.Read(exINI, pSection, "Vector.NormalRandomL"); + this->Vector_NormalRandomH.Read(exINI, pSection, "Vector.NormalRandomH"); + this->Vector_NormalFAnglePerStep.Read(exINI, pSection, "Vector.NormalFAnglePerStep"); + this->Vector_NormalLAnglePerStep.Read(exINI, pSection, "Vector.NormalLAnglePerStep"); + this->Vector_NormalHAnglePerStep.Read(exINI, pSection, "Vector.NormalHAnglePerStep"); + this->Vector_MoveTo.Read(exINI, pSection, "Vector.MoveTo"); + this->Vector_GrowRate.Read(exINI, pSection, "Vector.GrowRate"); + this->Vector_AnglePerStep.Read(exINI, pSection, "Vector.AnglePerStep"); + this->Vector_CircleRadius.Read(exINI, pSection, "Vector.CircleRadius"); + this->Vector_CircleSpeed.Read(exINI, pSection, "Vector.CircleSpeed"); + this->Vector_CircleSpeedAcceleration.Read(exINI, pSection, "Vector.CircleSpeedAcceleration"); + this->Vector_CircleMaxSpeed.Read(exINI, pSection, "Vector.CircleMaxSpeed"); + this->Vector_CircleMinSpeed.Read(exINI, pSection, "Vector.CircleMinSpeed"); + this->Vector_CircleAnglePerStep.Read(exINI, pSection, "Vector.CircleAnglePerStep"); + this->Vector_CircleAngleAcceleration.Read(exINI, pSection, "Vector.CircleAngleAcceleration"); + this->Vector_CircleOrigin.Read(exINI, pSection, "Vector.CircleOrigin"); + this->Vector_AllowOriginTilt.Read(exINI, pSection, "Vector.AllowOriginTilt"); + this->Vector_CircleRadiusGrow.Read(exINI, pSection, "Vector.CircleRadiusGrow"); + this->Vector_CircleMaxRadius.Read(exINI, pSection, "Vector.CircleMaxRadius"); + this->Vector_CircleMinRadius.Read(exINI, pSection, "Vector.CircleMinRadius"); + this->Vector_CircleEndOnMaxRadius.Read(exINI, pSection, "Vector.CircleEndOnMaxRadius"); + this->Vector_CircleEndOnMinRadius.Read(exINI, pSection, "Vector.CircleEndOnMinRadius"); + this->Vector_TargetFLH.Read(exINI, pSection, "Vector.TargetFLH"); + this->Vector_ReachTarget.Read(exINI, pSection, "Vector.ReachTarget"); + this->Vector_ReachTargetEarlyEnd.Read(exINI, pSection, "Vector.ReachTargetEarlyEnd"); + this->Vector_ArcHeight.Read(exINI, pSection, "Vector.ArcHeight"); + this->Vector_ArcPeakPercent.Read(exINI, pSection, "Vector.ArcPeakPercent"); + this->Vector_ArcPeakRandomPercent.Read(exINI, pSection, "Vector.ArcPeakRandomPercent"); + this->Vector_ArcRotation.Read(exINI, pSection, "Vector.ArcRotation"); + this->Vector_InitialSpeed.Read(exINI, pSection, "Vector.InitialSpeed"); + this->Vector_MaxSpeed.Read(exINI, pSection, "Vector.MaxSpeed"); + this->Vector_MinSpeed.Read(exINI, pSection, "Vector.MinSpeed"); + this->Vector_Acceleration.Read(exINI, pSection, "Vector.Acceleration"); + this->Vector_AllowFallingDestroy.Read(exINI, pSection, "Vector.AllowFallingDestroy"); + this->Vector_FallingDestroyHeight.Read(exINI, pSection, "Vector.FallingDestroyHeight"); + this->Vector_AffectTechno.Read(exINI, pSection, "Vector.AffectTechno"); + this->Vector_AffectBullets.Read(exINI, pSection, "Vector.AffectBullets"); + this->Vector_CircleRandomRadiusMin.Read(exINI, pSection, "Vector.CircleRandomRadius.Min"); + this->Vector_CircleRandomRadiusMax.Read(exINI, pSection, "Vector.CircleRandomRadius.Max"); + this->Vector_CircleRandomAngleMin.Read(exINI, pSection, "Vector.CircleRandomAngle.Min"); + this->Vector_CircleRandomAngleMax.Read(exINI, pSection, "Vector.CircleRandomAngle.Max"); + this->Vector_CircleMaxAngle.Read(exINI, pSection, "Vector.CircleMaxAngle"); + this->Vector_CircleMinAngle.Read(exINI, pSection, "Vector.CircleMinAngle"); + this->Vector_TargetOffsetFMin.Read(exINI, pSection, "Vector.TargetOffsetF.Min"); + this->Vector_TargetOffsetFMax.Read(exINI, pSection, "Vector.TargetOffsetF.Max"); + this->Vector_TargetOffsetLMin.Read(exINI, pSection, "Vector.TargetOffsetL.Min"); + this->Vector_TargetOffsetLMax.Read(exINI, pSection, "Vector.TargetOffsetL.Max"); + this->Vector_TargetOffsetHMin.Read(exINI, pSection, "Vector.TargetOffsetH.Min"); + this->Vector_TargetOffsetHMax.Read(exINI, pSection, "Vector.TargetOffsetH.Max"); + this->Vector_ArcRandomHeightMin.Read(exINI, pSection, "Vector.ArcRandomHeight.Min"); + this->Vector_ArcRandomHeightMax.Read(exINI, pSection, "Vector.ArcRandomHeight.Max"); + this->Vector_ArcRandomRotationMin.Read(exINI, pSection, "Vector.ArcRandomRotation.Min"); + this->Vector_ArcRandomRotationMax.Read(exINI, pSection, "Vector.ArcRandomRotation.Max"); + this->Vector_RandomSpeedMin.Read(exINI, pSection, "Vector.RandomSpeed.Min"); + this->Vector_RandomSpeedMax.Read(exINI, pSection, "Vector.RandomSpeed.Max"); + this->Vector_OriginMoveTo.Read(exINI, pSection, "Vector.Origin.MoveTo"); + this->Vector_OriginGrowRate.Read(exINI, pSection, "Vector.Origin.GrowRate"); + this->Vector_OriginTargetFLH.Read(exINI, pSection, "Vector.Origin.TargetFLH"); + this->Vector_OriginInitialSpeed.Read(exINI, pSection, "Vector.Origin.InitialSpeed"); + this->Vector_OriginReachTarget.Read(exINI, pSection, "Vector.Origin.ReachTarget"); + this->Vector_OriginArcHeight.Read(exINI, pSection, "Vector.Origin.ArcHeight"); + this->Vector_OriginCircleRadius.Read(exINI, pSection, "Vector.Origin.CircleRadius"); + this->Vector_OriginCircleSpeed.Read(exINI, pSection, "Vector.Origin.CircleSpeed"); + this->Vector_OriginCircleAnglePerStep.Read(exINI, pSection, "Vector.Origin.CircleAnglePerStep"); + this->Vector_OriginCircleRadiusGrow.Read(exINI, pSection, "Vector.Origin.CircleRadiusGrow"); + this->Vector_OriginCircleMaxRadius.Read(exINI, pSection, "Vector.Origin.CircleMaxRadius"); + this->Vector_OriginCircleMinRadius.Read(exINI, pSection, "Vector.Origin.CircleMinRadius"); + this->Vector_OriginCircleEndOnMaxRadius.Read(exINI, pSection, "Vector.Origin.CircleEndOnMaxRadius"); + this->Vector_OriginCircleEndOnMinRadius.Read(exINI, pSection, "Vector.Origin.CircleEndOnMinRadius"); + this->Vector_OriginNormalVector.Read(exINI, pSection, "Vector.Origin.NormalVector"); + this->Vector_OriginNormalFAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalFAnglePerStep"); + this->Vector_OriginNormalLAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalLAnglePerStep"); + this->Vector_OriginNormalHAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalHAnglePerStep"); + this->Vector_OriginAllowedTilt.Read(exINI, pSection, "Vector.Origin.AllowedTilt"); + this->Vector_OriginCircleOffset.Read(exINI, pSection, "Vector.Origin.CircleOffset"); + this->Vector_OriginOrigin.Read(exINI, pSection, "Vector.Origin.Origin"); + this->Vector_OriginOriginFLH.Read(exINI, pSection, "Vector.Origin.OriginFLH"); + this->Vector_NormalFAngleRMin.Read(exINI, pSection, "Vector.NormalFAngleRanges.Min"); + this->Vector_NormalFAngleRMax.Read(exINI, pSection, "Vector.NormalFAngleRanges.Max"); + this->Vector_NormalFAngleRMin2.Read(exINI, pSection, "Vector.NormalFAngleRanges.Min2"); + this->Vector_NormalFAngleRMax2.Read(exINI, pSection, "Vector.NormalFAngleRanges.Max2"); + this->Vector_NormalLAngleRMin.Read(exINI, pSection, "Vector.NormalLAngleRanges.Min"); + this->Vector_NormalLAngleRMax.Read(exINI, pSection, "Vector.NormalLAngleRanges.Max"); + this->Vector_NormalLAngleRMin2.Read(exINI, pSection, "Vector.NormalLAngleRanges.Min2"); + this->Vector_NormalLAngleRMax2.Read(exINI, pSection, "Vector.NormalLAngleRanges.Max2"); + this->Vector_NormalHAngleRMin.Read(exINI, pSection, "Vector.NormalHAngleRanges.Min"); + this->Vector_NormalHAngleRMax.Read(exINI, pSection, "Vector.NormalHAngleRanges.Max"); + this->Vector_NormalHAngleRMin2.Read(exINI, pSection, "Vector.NormalHAngleRanges.Min2"); + this->Vector_NormalHAngleRMax2.Read(exINI, pSection, "Vector.NormalHAngleRanges.Max2"); + this->Vector_OriginNormalFAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Min"); + this->Vector_OriginNormalFAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Max"); + this->Vector_OriginNormalFAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Min2"); + this->Vector_OriginNormalFAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Max2"); + this->Vector_OriginNormalLAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Min"); + this->Vector_OriginNormalLAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Max"); + this->Vector_OriginNormalLAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Min2"); + this->Vector_OriginNormalLAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Max2"); + this->Vector_OriginNormalHAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Min"); + this->Vector_OriginNormalHAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Max"); + this->Vector_OriginNormalHAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Min2"); + this->Vector_OriginNormalHAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Max2"); + // Groups exINI.ParseStringList(this->Groups, pSection, "Groups"); AddToGroupsMap(); @@ -214,6 +333,7 @@ void AttachEffectTypeClass::Serialize(T& Stm) .Process(this->ExpireWeapon_TriggerOn) .Process(this->ExpireWeapon_CumulativeOnlyOnce) .Process(this->ExpireWeapon_UseInvokerAsOwner) + .Process(this->Next) .Process(this->Tint_Color) .Process(this->Tint_Intensity) .Process(this->Tint_VisibleToHouses) @@ -248,6 +368,120 @@ void AttachEffectTypeClass::Serialize(T& Stm) .Process(this->DisableWeapons) .Process(this->Unkillable) .Process(this->LaserTrail_Type) + .Process(this->Vector_TimeStep) + .Process(this->Vector_DisabledFrames) + .Process(this->Vector_SyncFacing) + .Process(this->Vector_OriginIsOnWorld) + .Process(this->Vector_OriginIsOnBody) + .Process(this->Vector_Origin) + .Process(this->Vector_OriginFLH) + .Process(this->Vector_OriginNoUpdate) + .Process(this->Vector_Force) + .Process(this->Vector_Freeze) + .Process(this->Vector_AllowedTilt) + .Process(this->Vector_NormalVector) + .Process(this->Vector_NormalRandomF) + .Process(this->Vector_NormalRandomL) + .Process(this->Vector_NormalRandomH) + .Process(this->Vector_NormalFAnglePerStep) + .Process(this->Vector_NormalLAnglePerStep) + .Process(this->Vector_NormalHAnglePerStep) + .Process(this->Vector_MoveTo) + .Process(this->Vector_GrowRate) + .Process(this->Vector_AnglePerStep) + .Process(this->Vector_CircleRadius) + .Process(this->Vector_CircleSpeed) + .Process(this->Vector_CircleSpeedAcceleration) + .Process(this->Vector_CircleMaxSpeed) + .Process(this->Vector_CircleMinSpeed) + .Process(this->Vector_CircleAnglePerStep) + .Process(this->Vector_CircleAngleAcceleration) + .Process(this->Vector_CircleOrigin) + .Process(this->Vector_AllowOriginTilt) + .Process(this->Vector_CircleRadiusGrow) + .Process(this->Vector_CircleMaxRadius) + .Process(this->Vector_CircleMinRadius) + .Process(this->Vector_CircleEndOnMaxRadius) + .Process(this->Vector_CircleEndOnMinRadius) + .Process(this->Vector_TargetFLH) + .Process(this->Vector_ReachTarget) + .Process(this->Vector_ReachTargetEarlyEnd) + .Process(this->Vector_ArcHeight) + .Process(this->Vector_ArcPeakPercent) + .Process(this->Vector_ArcPeakRandomPercent) + .Process(this->Vector_ArcRotation) + .Process(this->Vector_InitialSpeed) + .Process(this->Vector_MaxSpeed) + .Process(this->Vector_MinSpeed) + .Process(this->Vector_Acceleration) + .Process(this->Vector_AllowFallingDestroy) + .Process(this->Vector_FallingDestroyHeight) + .Process(this->Vector_AffectTechno) + .Process(this->Vector_AffectBullets) + .Process(this->Vector_CircleRandomRadiusMin) + .Process(this->Vector_CircleRandomRadiusMax) + .Process(this->Vector_CircleRandomAngleMin) + .Process(this->Vector_CircleRandomAngleMax) + .Process(this->Vector_CircleMaxAngle) + .Process(this->Vector_CircleMinAngle) + .Process(this->Vector_TargetOffsetFMin) + .Process(this->Vector_TargetOffsetFMax) + .Process(this->Vector_TargetOffsetLMin) + .Process(this->Vector_TargetOffsetLMax) + .Process(this->Vector_TargetOffsetHMin) + .Process(this->Vector_TargetOffsetHMax) + .Process(this->Vector_ArcRandomHeightMin) + .Process(this->Vector_ArcRandomHeightMax) + .Process(this->Vector_ArcRandomRotationMin) + .Process(this->Vector_ArcRandomRotationMax) + .Process(this->Vector_RandomSpeedMin) + .Process(this->Vector_RandomSpeedMax) + .Process(this->Vector_OriginMoveTo) + .Process(this->Vector_OriginGrowRate) + .Process(this->Vector_OriginTargetFLH) + .Process(this->Vector_OriginInitialSpeed) + .Process(this->Vector_OriginReachTarget) + .Process(this->Vector_OriginArcHeight) + .Process(this->Vector_OriginCircleRadius) + .Process(this->Vector_OriginCircleSpeed) + .Process(this->Vector_OriginCircleAnglePerStep) + .Process(this->Vector_OriginCircleRadiusGrow) + .Process(this->Vector_OriginCircleMaxRadius) + .Process(this->Vector_OriginCircleMinRadius) + .Process(this->Vector_OriginCircleEndOnMaxRadius) + .Process(this->Vector_OriginCircleEndOnMinRadius) + .Process(this->Vector_OriginNormalVector) + .Process(this->Vector_OriginNormalFAnglePerStep) + .Process(this->Vector_OriginNormalLAnglePerStep) + .Process(this->Vector_OriginNormalHAnglePerStep) + .Process(this->Vector_OriginAllowedTilt) + .Process(this->Vector_OriginCircleOffset) + .Process(this->Vector_OriginOrigin) + .Process(this->Vector_OriginOriginFLH) + .Process(this->Vector_NormalFAngleRMin) + .Process(this->Vector_NormalFAngleRMax) + .Process(this->Vector_NormalFAngleRMin2) + .Process(this->Vector_NormalFAngleRMax2) + .Process(this->Vector_NormalLAngleRMin) + .Process(this->Vector_NormalLAngleRMax) + .Process(this->Vector_NormalLAngleRMin2) + .Process(this->Vector_NormalLAngleRMax2) + .Process(this->Vector_NormalHAngleRMin) + .Process(this->Vector_NormalHAngleRMax) + .Process(this->Vector_NormalHAngleRMin2) + .Process(this->Vector_NormalHAngleRMax2) + .Process(this->Vector_OriginNormalFAngleRMin) + .Process(this->Vector_OriginNormalFAngleRMax) + .Process(this->Vector_OriginNormalFAngleRMin2) + .Process(this->Vector_OriginNormalFAngleRMax2) + .Process(this->Vector_OriginNormalLAngleRMin) + .Process(this->Vector_OriginNormalLAngleRMax) + .Process(this->Vector_OriginNormalLAngleRMin2) + .Process(this->Vector_OriginNormalLAngleRMax2) + .Process(this->Vector_OriginNormalHAngleRMin) + .Process(this->Vector_OriginNormalHAngleRMax) + .Process(this->Vector_OriginNormalHAngleRMin2) + .Process(this->Vector_OriginNormalHAngleRMax2) .Process(this->Groups) ; } @@ -371,6 +605,30 @@ namespace detail return false; } + + template <> + inline bool read(VectorOrigin& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto str = parser.value(); + + if (!_strcmpi(str, "Self")) + value = VectorOrigin::Self; + else if (!_strcmpi(str, "Launcher")) + value = VectorOrigin::Launcher; + else if (!_strcmpi(str, "Target")) + value = VectorOrigin::Target; + else if (!_strcmpi(str, "Source")) + value = VectorOrigin::Source; + else + value = VectorOrigin::Self; + + return true; + } + + return false; + } } // AEAttachInfoTypeClass diff --git a/src/New/Type/AttachEffectTypeClass.h b/src/New/Type/AttachEffectTypeClass.h index 7d5a09243a..5f4c624bfd 100644 --- a/src/New/Type/AttachEffectTypeClass.h +++ b/src/New/Type/AttachEffectTypeClass.h @@ -36,6 +36,14 @@ enum class ExpireWeaponCondition : unsigned char MAKE_ENUM_FLAGS(ExpireWeaponCondition); +enum class VectorOrigin : int +{ + Self = 0, + Launcher = 1, + Target = 2, + Source = 3 +}; + class AttachEffectTypeClass final : public Enumerable { static std::unordered_map> GroupsMap; @@ -67,6 +75,7 @@ class AttachEffectTypeClass final : public Enumerable Valueable ExpireWeapon_TriggerOn; Valueable ExpireWeapon_CumulativeOnlyOnce; Valueable ExpireWeapon_UseInvokerAsOwner; + ValueableVector Next; Nullable Tint_Color; Valueable Tint_Intensity; Valueable Tint_VisibleToHouses; @@ -102,6 +111,121 @@ class AttachEffectTypeClass final : public Enumerable Valueable Unkillable; ValueableIdx LaserTrail_Type; + Valueable Vector_TimeStep; + Valueable Vector_DisabledFrames; + Valueable Vector_SyncFacing; + Valueable Vector_OriginIsOnWorld; + Valueable Vector_OriginIsOnBody; + Valueable Vector_Origin; + Nullable Vector_OriginFLH; + Valueable Vector_OriginNoUpdate; + Valueable Vector_Force; + Valueable Vector_Freeze; + Valueable Vector_AllowedTilt; + Nullable Vector_NormalVector; + Valueable Vector_NormalRandomF; + Valueable Vector_NormalRandomL; + Valueable Vector_NormalRandomH; + Valueable Vector_NormalFAnglePerStep; + Valueable Vector_NormalLAnglePerStep; + Valueable Vector_NormalHAnglePerStep; + Valueable Vector_MoveTo; + Nullable Vector_GrowRate; + Valueable Vector_AnglePerStep; + Valueable Vector_CircleRadius; + Valueable Vector_CircleSpeed; + Valueable Vector_CircleSpeedAcceleration; + Valueable Vector_CircleMaxSpeed; + Valueable Vector_CircleMinSpeed; + Valueable Vector_CircleAnglePerStep; + Valueable Vector_CircleAngleAcceleration; + Nullable Vector_CircleOrigin; + Valueable Vector_AllowOriginTilt; + Valueable Vector_CircleRadiusGrow; + Valueable Vector_CircleMaxRadius; + Valueable Vector_CircleMinRadius; + Valueable Vector_CircleEndOnMaxRadius; + Valueable Vector_CircleEndOnMinRadius; + Nullable Vector_TargetFLH; + Valueable Vector_ReachTarget; + Valueable Vector_ReachTargetEarlyEnd; + Valueable Vector_ArcHeight; + Valueable Vector_ArcPeakPercent; + Valueable Vector_ArcPeakRandomPercent; + Valueable Vector_ArcRotation; + Valueable Vector_InitialSpeed; + Valueable Vector_MaxSpeed; + Valueable Vector_MinSpeed; + Valueable Vector_Acceleration; + Valueable Vector_AllowFallingDestroy; + Valueable Vector_FallingDestroyHeight; + Valueable Vector_AffectTechno; + Valueable Vector_AffectBullets; + Valueable Vector_CircleRandomRadiusMin; + Valueable Vector_CircleRandomRadiusMax; + Valueable Vector_CircleRandomAngleMin; + Valueable Vector_CircleRandomAngleMax; + Valueable Vector_CircleMaxAngle; + Valueable Vector_CircleMinAngle; + Valueable Vector_TargetOffsetFMin; + Valueable Vector_TargetOffsetFMax; + Valueable Vector_TargetOffsetLMin; + Valueable Vector_TargetOffsetLMax; + Valueable Vector_TargetOffsetHMin; + Valueable Vector_TargetOffsetHMax; + Valueable Vector_ArcRandomHeightMin; + Valueable Vector_ArcRandomHeightMax; + Valueable Vector_ArcRandomRotationMin; + Valueable Vector_ArcRandomRotationMax; + Valueable Vector_RandomSpeedMin; + Valueable Vector_RandomSpeedMax; + Nullable Vector_OriginMoveTo; + Nullable Vector_OriginGrowRate; + Nullable Vector_OriginTargetFLH; + Valueable Vector_OriginInitialSpeed; + Valueable Vector_OriginReachTarget; + Valueable Vector_OriginArcHeight; + Valueable Vector_OriginCircleRadius; + Valueable Vector_OriginCircleSpeed; + Valueable Vector_OriginCircleAnglePerStep; + Valueable Vector_OriginCircleRadiusGrow; + Valueable Vector_OriginCircleMaxRadius; + Valueable Vector_OriginCircleMinRadius; + Valueable Vector_OriginCircleEndOnMaxRadius; + Valueable Vector_OriginCircleEndOnMinRadius; + Nullable Vector_OriginNormalVector; + Valueable Vector_OriginNormalFAnglePerStep; + Valueable Vector_OriginNormalLAnglePerStep; + Valueable Vector_OriginNormalHAnglePerStep; + Valueable Vector_OriginAllowedTilt; + Nullable Vector_OriginCircleOffset; + Valueable Vector_OriginOrigin; + Nullable Vector_OriginOriginFLH; + Valueable Vector_NormalFAngleRMin; + Valueable Vector_NormalFAngleRMax; + Valueable Vector_NormalFAngleRMin2; + Valueable Vector_NormalFAngleRMax2; + Valueable Vector_NormalLAngleRMin; + Valueable Vector_NormalLAngleRMax; + Valueable Vector_NormalLAngleRMin2; + Valueable Vector_NormalLAngleRMax2; + Valueable Vector_NormalHAngleRMin; + Valueable Vector_NormalHAngleRMax; + Valueable Vector_NormalHAngleRMin2; + Valueable Vector_NormalHAngleRMax2; + Valueable Vector_OriginNormalFAngleRMin; + Valueable Vector_OriginNormalFAngleRMax; + Valueable Vector_OriginNormalFAngleRMin2; + Valueable Vector_OriginNormalFAngleRMax2; + Valueable Vector_OriginNormalLAngleRMin; + Valueable Vector_OriginNormalLAngleRMax; + Valueable Vector_OriginNormalLAngleRMin2; + Valueable Vector_OriginNormalLAngleRMax2; + Valueable Vector_OriginNormalHAngleRMin; + Valueable Vector_OriginNormalHAngleRMax; + Valueable Vector_OriginNormalHAngleRMin2; + Valueable Vector_OriginNormalHAngleRMax2; + std::vector Groups; AttachEffectTypeClass(const char* const pTitle) : Enumerable(pTitle) @@ -131,6 +255,7 @@ class AttachEffectTypeClass final : public Enumerable , ExpireWeapon_TriggerOn { ExpireWeaponCondition::Expire } , ExpireWeapon_CumulativeOnlyOnce { false } , ExpireWeapon_UseInvokerAsOwner { false } + , Next {} , Tint_Color {} , Tint_Intensity { 0.0 } , Tint_VisibleToHouses { AffectedHouse::All } @@ -165,6 +290,120 @@ class AttachEffectTypeClass final : public Enumerable , DisableWeapons { false } , Unkillable { false } , LaserTrail_Type { -1 } + , Vector_TimeStep { 1 } + , Vector_DisabledFrames { 0 } + , Vector_SyncFacing { true } + , Vector_OriginIsOnWorld { false } + , Vector_OriginIsOnBody { false } + , Vector_Origin { VectorOrigin::Self } + , Vector_OriginFLH {} + , Vector_OriginNoUpdate { false } + , Vector_Force { true } + , Vector_Freeze { false } + , Vector_AllowedTilt { false } + , Vector_NormalVector {} + , Vector_NormalRandomF { 0.0 } + , Vector_NormalRandomL { 0.0 } + , Vector_NormalRandomH { 0.0 } + , Vector_NormalFAnglePerStep { 0.0 } + , Vector_NormalLAnglePerStep { 0.0 } + , Vector_NormalHAnglePerStep { 0.0 } + , Vector_MoveTo { CoordStruct::Empty } + , Vector_GrowRate {} + , Vector_AnglePerStep { 0.0 } + , Vector_CircleRadius { -1 } + , Vector_CircleSpeed { 0 } + , Vector_CircleSpeedAcceleration { 0 } + , Vector_CircleMaxSpeed { 0 } + , Vector_CircleMinSpeed { 0 } + , Vector_CircleAnglePerStep { 0.0 } + , Vector_CircleAngleAcceleration { 0.0 } + , Vector_CircleOrigin {} + , Vector_AllowOriginTilt { false } + , Vector_CircleRadiusGrow { 0 } + , Vector_CircleMaxRadius { 0 } + , Vector_CircleMinRadius { 0 } + , Vector_CircleEndOnMaxRadius { false } + , Vector_CircleEndOnMinRadius { false } + , Vector_TargetFLH {} + , Vector_ReachTarget { false } + , Vector_ReachTargetEarlyEnd { 0 } + , Vector_ArcHeight { 0 } + , Vector_ArcPeakPercent { 0.0 } + , Vector_ArcPeakRandomPercent { 0.0 } + , Vector_ArcRotation { 0.0 } + , Vector_InitialSpeed { -1 } + , Vector_MaxSpeed { -1 } + , Vector_MinSpeed { -1 } + , Vector_Acceleration { 0 } + , Vector_AllowFallingDestroy { false } + , Vector_FallingDestroyHeight { 0 } + , Vector_AffectTechno { true } + , Vector_AffectBullets { false } + , Vector_CircleRandomRadiusMin { -1 } + , Vector_CircleRandomRadiusMax { -1 } + , Vector_CircleRandomAngleMin { -1 } + , Vector_CircleRandomAngleMax { -1 } + , Vector_CircleMaxAngle { 0.0 } + , Vector_CircleMinAngle { 0.0 } + , Vector_TargetOffsetFMin { 0 } + , Vector_TargetOffsetFMax { 0 } + , Vector_TargetOffsetLMin { 0 } + , Vector_TargetOffsetLMax { 0 } + , Vector_TargetOffsetHMin { 0 } + , Vector_TargetOffsetHMax { 0 } + , Vector_ArcRandomHeightMin { 0 } + , Vector_ArcRandomHeightMax { 0 } + , Vector_ArcRandomRotationMin { 0.0 } + , Vector_ArcRandomRotationMax { 0.0 } + , Vector_RandomSpeedMin { -1 } + , Vector_RandomSpeedMax { -1 } + , Vector_OriginMoveTo {} + , Vector_OriginGrowRate {} + , Vector_OriginTargetFLH {} + , Vector_OriginInitialSpeed { -1 } + , Vector_OriginReachTarget { false } + , Vector_OriginArcHeight { 0 } + , Vector_OriginCircleRadius { -1 } + , Vector_OriginCircleSpeed { 0 } + , Vector_OriginCircleAnglePerStep { 0.0 } + , Vector_OriginCircleRadiusGrow { 0 } + , Vector_OriginCircleMaxRadius { 0 } + , Vector_OriginCircleMinRadius { 0 } + , Vector_OriginCircleEndOnMaxRadius { false } + , Vector_OriginCircleEndOnMinRadius { false } + , Vector_OriginNormalVector {} + , Vector_OriginNormalFAnglePerStep { 0.0 } + , Vector_OriginNormalLAnglePerStep { 0.0 } + , Vector_OriginNormalHAnglePerStep { 0.0 } + , Vector_OriginAllowedTilt { false } + , Vector_OriginCircleOffset {} + , Vector_OriginOrigin { VectorOrigin::Self } + , Vector_OriginOriginFLH {} + , Vector_NormalFAngleRMin { 0.0 } + , Vector_NormalFAngleRMax { 0.0 } + , Vector_NormalFAngleRMin2 { 0.0 } + , Vector_NormalFAngleRMax2 { 0.0 } + , Vector_NormalLAngleRMin { 0.0 } + , Vector_NormalLAngleRMax { 0.0 } + , Vector_NormalLAngleRMin2 { 0.0 } + , Vector_NormalLAngleRMax2 { 0.0 } + , Vector_NormalHAngleRMin { 0.0 } + , Vector_NormalHAngleRMax { 0.0 } + , Vector_NormalHAngleRMin2 { 0.0 } + , Vector_NormalHAngleRMax2 { 0.0 } + , Vector_OriginNormalFAngleRMin { 0.0 } + , Vector_OriginNormalFAngleRMax { 0.0 } + , Vector_OriginNormalFAngleRMin2 { 0.0 } + , Vector_OriginNormalFAngleRMax2 { 0.0 } + , Vector_OriginNormalLAngleRMin { 0.0 } + , Vector_OriginNormalLAngleRMax { 0.0 } + , Vector_OriginNormalLAngleRMin2 { 0.0 } + , Vector_OriginNormalLAngleRMax2 { 0.0 } + , Vector_OriginNormalHAngleRMin { 0.0 } + , Vector_OriginNormalHAngleRMax { 0.0 } + , Vector_OriginNormalHAngleRMin2 { 0.0 } + , Vector_OriginNormalHAngleRMax2 { 0.0 } , Groups {} {}; @@ -173,6 +412,20 @@ class AttachEffectTypeClass final : public Enumerable return this->Tint_Color.isset() || this->Tint_Intensity != 0.0; } + bool HasVector() const + { + return static_cast(this->Vector_MoveTo) != CoordStruct::Empty + || this->Vector_TargetFLH.isset() + || this->Vector_Freeze + || this->Vector_ReachTarget + || this->Vector_CircleRadius > 0 + || this->Vector_CircleSpeed != 0 + || this->Vector_CircleAnglePerStep > 0.0 + || (this->Vector_CircleRandomRadiusMax > this->Vector_CircleRandomRadiusMin) + || (this->Vector_CircleRandomAngleMax > this->Vector_CircleRandomAngleMin) + || this->Vector_OriginMoveTo.isset(); + } + bool HasGroup(const std::string& groupID) const; bool HasGroups(const std::vector& groupIDs, bool requireAll) const; From 6d90d01ef3da8ce249d5833f2af849e106cdde56 Mon Sep 17 00:00:00 2001 From: KenosisM Date: Fri, 26 Jun 2026 23:38:15 +0800 Subject: [PATCH 02/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86Arc=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E6=B2=A1=E6=9C=89=E8=AE=A1=E7=AE=97=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E9=AB=98=E5=BA=A6=E5=B7=AE=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=E4=BA=86PeakPercent=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E7=A7=BB=E5=8A=A8=E5=BC=A7=E7=BA=BF=E9=A1=B6?= =?UTF-8?q?=E5=B3=B0=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/New/Entity/VectorState.cpp | 54 ++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp index c440167842..88b3973c0b 100644 --- a/src/New/Entity/VectorState.cpp +++ b/src/New/Entity/VectorState.cpp @@ -540,6 +540,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat pB->Velocity.X = moveDisp.X; pB->Velocity.Y = moveDisp.Y; pB->Velocity.Z = moveDisp.Z; + s.StoredDisp = { currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }; } else { @@ -585,6 +586,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat { auto const pB = static_cast(pObject); pB->Velocity.X = moveDisp.X; pB->Velocity.Y = moveDisp.Y; pB->Velocity.Z = moveDisp.Z; + s.StoredDisp = { currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }; } else { @@ -643,12 +645,14 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (pType->Vector_ReachTarget) { - int totalDuration = isBullet ? 60 : 60; + int totalDuration = pType->Duration / pType->Vector_TimeStep; + if (totalDuration < 1) totalDuration = 1; int effectiveDuration = totalDuration - pType->Vector_DisabledFrames; if (effectiveDuration < 1) effectiveDuration = 1; - int remainingFrames = effectiveDuration - s.MovementFrames + 1; + int remainingFrames = effectiveDuration - s.MovementFrames; - if (pType->Vector_ReachTargetEarlyEnd > 0 && remainingFrames <= pType->Vector_ReachTargetEarlyEnd) + if (pType->Vector_ReachTargetEarlyEnd > 0 && pType->Vector_ReachTargetEarlyEnd < effectiveDuration + && remainingFrames <= pType->Vector_ReachTargetEarlyEnd) { s.DisabledTimer = pType->Vector_ReachTargetEarlyEnd; return; @@ -656,9 +660,12 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (remainingFrames <= 0) { - resultDisp.X = frameTarget.X - currentPos.X; - resultDisp.Y = frameTarget.Y - currentPos.Y; - resultDisp.Z = frameTarget.Z - currentPos.Z; + if (pType->Vector_Force) + { + resultDisp.X = frameTarget.X - currentPos.X; + resultDisp.Y = frameTarget.Y - currentPos.Y; + resultDisp.Z = frameTarget.Z - currentPos.Z; + } } else if (dirLen > 1e-6) { @@ -669,14 +676,28 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (s.ArcHeight != 0) { - double t = static_cast(s.MovementFrames - 1) / effectiveDuration; - double tNext = static_cast(s.MovementFrames) / effectiveDuration; - double peakScale = pType->Vector_ArcPeakPercent > 0 ? 1.0 / (pType->Vector_ArcPeakPercent * (1.0 - pType->Vector_ArcPeakPercent)) : 4.0; - double delta = peakScale * s.ArcHeight * (tNext * (1.0 - tNext) - t * (1.0 - t)); + double t = static_cast(s.MovementFrames) / effectiveDuration; + double peakPct = pType->Vector_ArcPeakPercent > 0 ? pType->Vector_ArcPeakPercent : 0.5; + double arcOffset; + if (t <= peakPct) + { + double u = t / peakPct; + arcOffset = s.ArcHeight * u * (2.0 - u); + } + else + { + double u = (t - peakPct) / (1.0 - peakPct); + arcOffset = s.ArcHeight * (1.0 - u * u); + } + double baseX = s.InitialLocation.X + (frameTarget.X - s.InitialLocation.X) * t; + double baseY = s.InitialLocation.Y + (frameTarget.Y - s.InitialLocation.Y) * t; + double baseZ = s.InitialLocation.Z + (frameTarget.Z - s.InitialLocation.Z) * t; if (s.ArcRotation == 0.0) { - resultDisp.Z += static_cast(delta); + resultDisp.X = static_cast(baseX - currentPos.X); + resultDisp.Y = static_cast(baseY - currentPos.Y); + resultDisp.Z = static_cast(baseZ + arcOffset - currentPos.Z); } else { @@ -697,13 +718,15 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat double rx = pnx * c + (dny * pnz - dnz * pny) * s_arc; double ry = pny * c + (dnz * pnx - dnx * pnz) * s_arc; double rz = pnz * c + (dnx * pny - dny * pnx) * s_arc; - resultDisp.X += static_cast(rx * delta); - resultDisp.Y += static_cast(ry * delta); - resultDisp.Z += static_cast(rz * delta); + resultDisp.X = static_cast(baseX + rx * arcOffset - currentPos.X); + resultDisp.Y = static_cast(baseY + ry * arcOffset - currentPos.Y); + resultDisp.Z = static_cast(baseZ + rz * arcOffset - currentPos.Z); } else { - resultDisp.Z += static_cast(delta); + resultDisp.X = static_cast(baseX - currentPos.X); + resultDisp.Y = static_cast(baseY - currentPos.Y); + resultDisp.Z = static_cast(baseZ + arcOffset - currentPos.Z); } } } @@ -726,6 +749,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat pB->Velocity.X = resultDisp.X; pB->Velocity.Y = resultDisp.Y; pB->Velocity.Z = resultDisp.Z; + s.StoredDisp = { currentPos.X + resultDisp.X, currentPos.Y + resultDisp.Y, currentPos.Z + resultDisp.Z }; } else { From fb44e47925d9bcf317174883782637136a47e40b Mon Sep 17 00:00:00 2001 From: KenosisM Date: Sat, 27 Jun 2026 00:07:12 +0800 Subject: [PATCH 03/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8DVector=20Arc=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E8=AE=A1=E7=AE=97=E5=88=9D=E5=A7=8B=E9=AB=98=E5=BA=A6?= =?UTF-8?q?=E5=B7=AE=E5=92=8CPeakPercent=E4=B8=8D=E8=83=BD=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E7=A7=BB=E5=8A=A8=E5=BC=A7=E9=A1=B6=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E3=80=82=E7=A7=BB=E9=99=A4Velocity=E8=B5=8B=E5=80=BC?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E5=BC=95=E6=93=8E=E8=AF=AF=E5=88=A4=E6=8A=9B?= =?UTF-8?q?=E5=B0=84=E4=BD=93=E5=8D=A1=E4=BD=8F=E5=AF=BC=E8=87=B4=E6=8F=90?= =?UTF-8?q?=E5=89=8D=E7=88=86=E7=82=B8=E3=80=82=E7=A7=BB=E9=99=A4VEC?= =?UTF-8?q?=E8=B0=83=E6=B5=8B=E6=97=A5=E5=BF=97=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Ext/Bullet/Hooks.DetonateLogics.cpp | 5 +++ src/Ext/Bullet/Hooks.cpp | 57 ++++++++++++++++++++++++- src/New/Entity/VectorState.cpp | 3 -- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/Ext/Bullet/Hooks.DetonateLogics.cpp b/src/Ext/Bullet/Hooks.DetonateLogics.cpp index e4b1f505ba..e7dab77c6d 100644 --- a/src/Ext/Bullet/Hooks.DetonateLogics.cpp +++ b/src/Ext/Bullet/Hooks.DetonateLogics.cpp @@ -83,6 +83,11 @@ DEFINE_HOOK(0x4690C1, BulletClass_Logics_DetonateOnAllMapObjects, 0x8) GET(BulletClass*, pThis, ESI); + // Debug::Log("[VEC] F=%d EXPLODE POS=(%d,%d,%d) TGT=(%d,%d,%d)\n", + // Unsorted::CurrentFrame, + // pThis->Location.X, pThis->Location.Y, pThis->Location.Z, + // pThis->TargetCoords.X, pThis->TargetCoords.Y, pThis->TargetCoords.Z); + auto const pWHExt = WarheadTypeExt::ExtMap.Find(pThis->WH); if (pWHExt->DetonateOnAllMapObjects && !pWHExt->WasDetonatedOnAllMapObjects diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index 9c3617846b..84dde19de8 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -59,10 +59,18 @@ DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6) if (pBulletExt->VectorType) { - if (pBulletExt->Vector.CurrentFrame >= pBulletExt->VectorType->Duration) + if (pBulletExt->Vector.CurrentFrame > pBulletExt->VectorType->Duration) pBulletExt->VectorType = nullptr; - else + else { + // Debug::Log("[VEC] F=%d AI POS=(%d,%d,%d) VEL=(%.0f,%.0f,%.0f) SRC=(%d,%d,%d) TGT=(%d,%d,%d) CF=%d\n", + // Unsorted::CurrentFrame, + // pThis->Location.X, pThis->Location.Y, pThis->Location.Z, + // pThis->Velocity.X, pThis->Velocity.Y, pThis->Velocity.Z, + // pThis->SourceCoords.X, pThis->SourceCoords.Y, pThis->SourceCoords.Z, + // pThis->TargetCoords.X, pThis->TargetCoords.Y, pThis->TargetCoords.Z, + // pBulletExt->Vector.CurrentFrame); pBulletExt->VectorAI(); + } } if (pBulletExt->InterceptedStatus & InterceptedStatus::Targeted) @@ -161,6 +169,48 @@ DEFINE_HOOK(0x466897, BulletClass_AI_Trailer, 0x6) return SkipGameCode; } +DEFINE_HOOK_AGAIN(0x467FEE, BulletClass_AI_UpdateEnd, 0x6) +DEFINE_HOOK(0x466781, BulletClass_AI_UpdateEnd, 0x6) +{ + GET(BulletClass*, pThis, EBP); + auto const pExt = BulletExt::ExtMap.Find(pThis); + + // Debug::Log("[VEC] F=%d UE STORED=(%d,%d,%d) POS=(%d,%d,%d) VEL=(%.0f,%.0f,%.0f) SRC=(%d,%d,%d) TGT=(%d,%d,%d)\n", + // Unsorted::CurrentFrame, + // pExt ? pExt->Vector.StoredDisp.X : -1, + // pExt ? pExt->Vector.StoredDisp.Y : -1, + // pExt ? pExt->Vector.StoredDisp.Z : -1, + // pThis->Location.X, pThis->Location.Y, pThis->Location.Z, + // pThis->Velocity.X, pThis->Velocity.Y, pThis->Velocity.Z, + // pThis->SourceCoords.X, pThis->SourceCoords.Y, pThis->SourceCoords.Z, + // pThis->TargetCoords.X, pThis->TargetCoords.Y, pThis->TargetCoords.Z); + + if (pExt && pExt->VectorType) + { + auto& pos = pExt->Vector.StoredDisp; + if (pos.X != 0 || pos.Y != 0 || pos.Z != 0) + { + CoordStruct prev = pThis->GetCoords(); + pThis->SetLocation(pos); + pThis->SourceCoords = pos; + if (pExt->VectorType->Vector_SyncFacing) + { + double dx = (double)(pos.X - prev.X); + double dy = (double)(pos.Y - prev.Y); + double dz = (double)(pos.Z - prev.Z); + double mag = std::sqrt(dx * dx + dy * dy + dz * dz); + if (mag > 1e-6) { dx /= mag; dy /= mag; dz /= mag; } + pThis->Velocity.X = dx; + pThis->Velocity.Y = dy; + pThis->Velocity.Z = dz; + } + pos = CoordStruct::Empty; + } + } + + return 0; +} + // Inviso bullets behave differently in BulletClass::AI when their target is bullet and // seemingly (at least partially) adopt characteristics of a vertical projectile. // This is a potentially slightly hacky solution to that, as proper solution @@ -490,6 +540,9 @@ DEFINE_HOOK(0x468D3F, BulletClass_ShouldExplode_AirTarget, 0x6) auto const pExt = BulletExt::ExtMap.Find(pThis); + if (pExt->VectorType) + return SkipCheck; + if (pExt->Trajectory && CheckTrajectoryCanNotAlwaysSnap(pExt->Trajectory->Flag())) return SkipCheck; diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp index 88b3973c0b..084682c29a 100644 --- a/src/New/Entity/VectorState.cpp +++ b/src/New/Entity/VectorState.cpp @@ -746,9 +746,6 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (isBullet) { auto const pB = static_cast(pObject); - pB->Velocity.X = resultDisp.X; - pB->Velocity.Y = resultDisp.Y; - pB->Velocity.Z = resultDisp.Z; s.StoredDisp = { currentPos.X + resultDisp.X, currentPos.Y + resultDisp.Y, currentPos.Z + resultDisp.Z }; } else From 293ec50a163daa1a2b74433a8b0668deff7a24db Mon Sep 17 00:00:00 2001 From: KenosisM Date: Sat, 27 Jun 2026 00:36:04 +0800 Subject: [PATCH 04/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8DPhobos=E7=89=88Vector?= =?UTF-8?q?=E5=BC=A7=E9=AB=98=E8=AE=A1=E7=AE=97=EF=BC=9AArcPeakPercent?= =?UTF-8?q?=E7=BC=BA=E5=B0=91/100=E5=AF=BC=E8=87=B4=E5=BC=A7=E7=BA=BF?= =?UTF-8?q?=E5=8F=98=E7=9B=B4=E7=BA=BF=E3=80=82=E7=A7=BB=E9=99=A4VEC?= =?UTF-8?q?=E8=B0=83=E6=B5=8B=E6=97=A5=E5=BF=97=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Ext/Bullet/Body.cpp | 1 + src/New/Entity/AttachEffectClass.cpp | 1 + src/New/Entity/VectorState.cpp | 5 ++++- src/New/Entity/VectorState.h | 4 ++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Ext/Bullet/Body.cpp b/src/Ext/Bullet/Body.cpp index 8806254ef6..c46a99c460 100644 --- a/src/Ext/Bullet/Body.cpp +++ b/src/Ext/Bullet/Body.cpp @@ -503,6 +503,7 @@ void BulletExt::ExtData::Serialize(T& Stm) .Process(this->Vector.PrevCirclePos) .Process(this->Vector.ArcHeight) .Process(this->Vector.ArcRotation) + .Process(this->Vector.ArcPeakPercent) .Process(this->Vector.TargetOffset) .Process(this->Vector.NormalRotF) .Process(this->Vector.NormalRotL) diff --git a/src/New/Entity/AttachEffectClass.cpp b/src/New/Entity/AttachEffectClass.cpp index 7fbfcc8c19..8b80d81e0f 100644 --- a/src/New/Entity/AttachEffectClass.cpp +++ b/src/New/Entity/AttachEffectClass.cpp @@ -1124,6 +1124,7 @@ bool AttachEffectClass::Serialize(T& Stm) .Process(this->Vector.PrevCirclePos) .Process(this->Vector.ArcHeight) .Process(this->Vector.ArcRotation) + .Process(this->Vector.ArcPeakPercent) .Process(this->Vector.TargetOffset) .Process(this->Vector.NormalRotF) .Process(this->Vector.NormalRotL) diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp index 084682c29a..afbb141098 100644 --- a/src/New/Entity/VectorState.cpp +++ b/src/New/Entity/VectorState.cpp @@ -84,6 +84,9 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat s.ArcRotation = pType->Vector_ArcRotation; if (pType->Vector_ArcRandomRotationMin != pType->Vector_ArcRandomRotationMax) s.ArcRotation = V_Random(pType->Vector_ArcRandomRotationMin, pType->Vector_ArcRandomRotationMax); + s.ArcPeakPercent = (pType->Vector_ArcPeakRandomPercent > 0 ? pType->Vector_ArcPeakRandomPercent : pType->Vector_ArcPeakPercent) / 100.0; + if (s.ArcPeakPercent <= 0.0) s.ArcPeakPercent = 0.5; + if (s.ArcPeakPercent >= 1.0) s.ArcPeakPercent = 0.5; if (pType->Vector_TargetFLH.isset()) { @@ -677,7 +680,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (s.ArcHeight != 0) { double t = static_cast(s.MovementFrames) / effectiveDuration; - double peakPct = pType->Vector_ArcPeakPercent > 0 ? pType->Vector_ArcPeakPercent : 0.5; + double peakPct = s.ArcPeakPercent > 0 ? s.ArcPeakPercent : 0.5; double arcOffset; if (t <= peakPct) { diff --git a/src/New/Entity/VectorState.h b/src/New/Entity/VectorState.h index cc08bed4fd..1d6b0f3e2a 100644 --- a/src/New/Entity/VectorState.h +++ b/src/New/Entity/VectorState.h @@ -19,6 +19,7 @@ struct VectorState CoordStruct PrevCirclePos; double ArcHeight; double ArcRotation; + double ArcPeakPercent; CoordStruct TargetOffset; double NormalRotF; double NormalRotL; @@ -46,6 +47,7 @@ struct VectorState double OriginNormalStepH; double OriginFacing; double OriginTilt; + CoordStruct StoredDisp; VectorState() : Initialized { false } @@ -61,6 +63,7 @@ struct VectorState , PrevCirclePos { CoordStruct::Empty } , ArcHeight { 0.0 } , ArcRotation { 0.0 } + , ArcPeakPercent { 0.5 } , TargetOffset { CoordStruct::Empty } , NormalRotF { 0.0 } , NormalRotL { 0.0 } @@ -88,6 +91,7 @@ struct VectorState , OriginNormalStepH { 0.0 } , OriginFacing { 0.0 } , OriginTilt { 0.0 } + , StoredDisp { CoordStruct::Empty } { } }; From 190502bd49f3b6e343444c76a7877440bdbec75a Mon Sep 17 00:00:00 2001 From: KenosisM Date: Sat, 27 Jun 2026 00:46:24 +0800 Subject: [PATCH 05/11] =?UTF-8?q?Phobos=E7=89=88Vector=EF=BC=9A=E6=96=B0?= =?UTF-8?q?=E5=A2=9EArcPeakPercent/Min/Max=E9=9A=8F=E6=9C=BA=E5=B3=B0?= =?UTF-8?q?=E4=BD=8D=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/New/Entity/VectorState.cpp | 7 ++++++- src/New/Type/AttachEffectTypeClass.cpp | 4 ++++ src/New/Type/AttachEffectTypeClass.h | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp index afbb141098..6f64a7ff82 100644 --- a/src/New/Entity/VectorState.cpp +++ b/src/New/Entity/VectorState.cpp @@ -84,7 +84,12 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat s.ArcRotation = pType->Vector_ArcRotation; if (pType->Vector_ArcRandomRotationMin != pType->Vector_ArcRandomRotationMax) s.ArcRotation = V_Random(pType->Vector_ArcRandomRotationMin, pType->Vector_ArcRandomRotationMax); - s.ArcPeakPercent = (pType->Vector_ArcPeakRandomPercent > 0 ? pType->Vector_ArcPeakRandomPercent : pType->Vector_ArcPeakPercent) / 100.0; + if (pType->Vector_ArcPeakRandomPercentMin > 0 && pType->Vector_ArcPeakRandomPercentMax > pType->Vector_ArcPeakRandomPercentMin) + s.ArcPeakPercent = V_Random(pType->Vector_ArcPeakRandomPercentMin, pType->Vector_ArcPeakRandomPercentMax) / 100.0; + else if (pType->Vector_ArcPeakRandomPercent > 0) + s.ArcPeakPercent = pType->Vector_ArcPeakRandomPercent / 100.0; + else + s.ArcPeakPercent = pType->Vector_ArcPeakPercent / 100.0; if (s.ArcPeakPercent <= 0.0) s.ArcPeakPercent = 0.5; if (s.ArcPeakPercent >= 1.0) s.ArcPeakPercent = 0.5; diff --git a/src/New/Type/AttachEffectTypeClass.cpp b/src/New/Type/AttachEffectTypeClass.cpp index 76b7d8113a..8c4301a84c 100644 --- a/src/New/Type/AttachEffectTypeClass.cpp +++ b/src/New/Type/AttachEffectTypeClass.cpp @@ -224,6 +224,8 @@ void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) this->Vector_ArcHeight.Read(exINI, pSection, "Vector.ArcHeight"); this->Vector_ArcPeakPercent.Read(exINI, pSection, "Vector.ArcPeakPercent"); this->Vector_ArcPeakRandomPercent.Read(exINI, pSection, "Vector.ArcPeakRandomPercent"); + this->Vector_ArcPeakRandomPercentMin.Read(exINI, pSection, "Vector.ArcPeakRandomPercent.Min"); + this->Vector_ArcPeakRandomPercentMax.Read(exINI, pSection, "Vector.ArcPeakRandomPercent.Max"); this->Vector_ArcRotation.Read(exINI, pSection, "Vector.ArcRotation"); this->Vector_InitialSpeed.Read(exINI, pSection, "Vector.InitialSpeed"); this->Vector_MaxSpeed.Read(exINI, pSection, "Vector.MaxSpeed"); @@ -409,6 +411,8 @@ void AttachEffectTypeClass::Serialize(T& Stm) .Process(this->Vector_ArcHeight) .Process(this->Vector_ArcPeakPercent) .Process(this->Vector_ArcPeakRandomPercent) + .Process(this->Vector_ArcPeakRandomPercentMin) + .Process(this->Vector_ArcPeakRandomPercentMax) .Process(this->Vector_ArcRotation) .Process(this->Vector_InitialSpeed) .Process(this->Vector_MaxSpeed) diff --git a/src/New/Type/AttachEffectTypeClass.h b/src/New/Type/AttachEffectTypeClass.h index 5f4c624bfd..727c2ea05c 100644 --- a/src/New/Type/AttachEffectTypeClass.h +++ b/src/New/Type/AttachEffectTypeClass.h @@ -152,6 +152,8 @@ class AttachEffectTypeClass final : public Enumerable Valueable Vector_ArcHeight; Valueable Vector_ArcPeakPercent; Valueable Vector_ArcPeakRandomPercent; + Valueable Vector_ArcPeakRandomPercentMin; + Valueable Vector_ArcPeakRandomPercentMax; Valueable Vector_ArcRotation; Valueable Vector_InitialSpeed; Valueable Vector_MaxSpeed; @@ -331,6 +333,8 @@ class AttachEffectTypeClass final : public Enumerable , Vector_ArcHeight { 0 } , Vector_ArcPeakPercent { 0.0 } , Vector_ArcPeakRandomPercent { 0.0 } + , Vector_ArcPeakRandomPercentMin { 0.0 } + , Vector_ArcPeakRandomPercentMax { 0.0 } , Vector_ArcRotation { 0.0 } , Vector_InitialSpeed { -1 } , Vector_MaxSpeed { -1 } From da9b8e036af18273969e5f3d1246e841f31cb103 Mon Sep 17 00:00:00 2001 From: KenosisM Date: Sat, 27 Jun 2026 00:47:00 +0800 Subject: [PATCH 06/11] =?UTF-8?q?Phobos=E7=89=88Vector=EF=BC=9A=E6=94=AF?= =?UTF-8?q?=E6=8C=81Next=E4=B8=B2=E8=81=94=EF=BC=8C=E5=BD=93=E5=89=8DVecto?= =?UTF-8?q?r=E8=BF=87=E6=9C=9F=E6=97=B6=E8=87=AA=E5=8A=A8=E9=99=84?= =?UTF-8?q?=E5=8A=A0=E4=B8=8B=E4=B8=80=E4=B8=AAVector=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Ext/Bullet/Hooks.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index 84dde19de8..79a1df15cd 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -60,7 +60,21 @@ DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6) if (pBulletExt->VectorType) { if (pBulletExt->Vector.CurrentFrame > pBulletExt->VectorType->Duration) - pBulletExt->VectorType = nullptr; + { + bool foundNext = false; + for (auto pNextType : pBulletExt->VectorType->Next) + { + if (pNextType && pNextType->HasVector() && pNextType->Vector_AffectBullets) + { + pBulletExt->VectorType = pNextType; + pBulletExt->Vector = {}; + foundNext = true; + break; + } + } + if (!foundNext) + pBulletExt->VectorType = nullptr; + } else { // Debug::Log("[VEC] F=%d AI POS=(%d,%d,%d) VEL=(%.0f,%.0f,%.0f) SRC=(%d,%d,%d) TGT=(%d,%d,%d) CF=%d\n", // Unsorted::CurrentFrame, From e162ce8f6f48bdafc1d49f7d35c5ea1dd9027374 Mon Sep 17 00:00:00 2001 From: KenosisM Date: Sat, 27 Jun 2026 01:01:07 +0800 Subject: [PATCH 07/11] =?UTF-8?q?Vector:=20=E6=94=AF=E6=8C=81Next=E4=B8=B2?= =?UTF-8?q?=E8=81=94(Duration-EarlyEnd=E5=90=8E=E8=87=AA=E5=8A=A8=E5=88=87?= =?UTF-8?q?=E6=8D=A2)=E3=80=82=E7=A7=BB=E9=99=A4Circle/MoveTo=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E7=9A=84Velocity=E8=B5=8B=E5=80=BC=EF=BC=8C=E9=98=B2?= =?UTF-8?q?=E6=AD=A2=E5=BC=95=E6=93=8E=E8=AF=AF=E5=88=A4=E6=8A=9B=E5=B0=84?= =?UTF-8?q?=E4=BD=93=E5=8D=A1=E4=BD=8F=E6=8F=90=E5=89=8D=E5=BC=95=E7=88=86?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Ext/Bullet/Hooks.cpp | 12 ++++-------- src/New/Entity/VectorState.cpp | 6 ------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index 79a1df15cd..1c9c04d6a7 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -59,7 +59,10 @@ DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6) if (pBulletExt->VectorType) { - if (pBulletExt->Vector.CurrentFrame > pBulletExt->VectorType->Duration) + int earlyEnd = pBulletExt->VectorType->Vector_ReachTargetEarlyEnd; + int endFrame = pBulletExt->VectorType->Duration - earlyEnd; + if (endFrame < 1) endFrame = pBulletExt->VectorType->Duration; + if (pBulletExt->Vector.CurrentFrame >= endFrame || pBulletExt->Vector.CurrentFrame > pBulletExt->VectorType->Duration) { bool foundNext = false; for (auto pNextType : pBulletExt->VectorType->Next) @@ -76,13 +79,6 @@ DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6) pBulletExt->VectorType = nullptr; } else { - // Debug::Log("[VEC] F=%d AI POS=(%d,%d,%d) VEL=(%.0f,%.0f,%.0f) SRC=(%d,%d,%d) TGT=(%d,%d,%d) CF=%d\n", - // Unsorted::CurrentFrame, - // pThis->Location.X, pThis->Location.Y, pThis->Location.Z, - // pThis->Velocity.X, pThis->Velocity.Y, pThis->Velocity.Z, - // pThis->SourceCoords.X, pThis->SourceCoords.Y, pThis->SourceCoords.Z, - // pThis->TargetCoords.X, pThis->TargetCoords.Y, pThis->TargetCoords.Z, - // pBulletExt->Vector.CurrentFrame); pBulletExt->VectorAI(); } } diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp index 6f64a7ff82..ba98f5d663 100644 --- a/src/New/Entity/VectorState.cpp +++ b/src/New/Entity/VectorState.cpp @@ -544,10 +544,6 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (isBullet) { - auto const pB = static_cast(pObject); - pB->Velocity.X = moveDisp.X; - pB->Velocity.Y = moveDisp.Y; - pB->Velocity.Z = moveDisp.Z; s.StoredDisp = { currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }; } else @@ -592,8 +588,6 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (isBullet) { - auto const pB = static_cast(pObject); - pB->Velocity.X = moveDisp.X; pB->Velocity.Y = moveDisp.Y; pB->Velocity.Z = moveDisp.Z; s.StoredDisp = { currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }; } else From 56a389f6f980348e6ade916567931afc876dfa8e Mon Sep 17 00:00:00 2001 From: KenosisM Date: Sat, 27 Jun 2026 01:05:55 +0800 Subject: [PATCH 08/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8DNext=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E6=97=B6InitialOriginPos=E4=B8=8D=E9=9A=8FOrigin=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=9B=B4=E6=96=B0=EF=BC=8C=E5=AF=BC=E8=87=B4Circle?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E5=9B=9E=E5=88=B0Launcher=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/New/Entity/VectorState.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp index ba98f5d663..db67aebb09 100644 --- a/src/New/Entity/VectorState.cpp +++ b/src/New/Entity/VectorState.cpp @@ -56,6 +56,19 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat { s.Initialized = true; s.InitialOriginPos = GetPos(); + switch (pType->Vector_Origin) + { + case VectorOrigin::Launcher: + if (pInvoker) s.InitialOriginPos = pInvoker->GetCoords(); + break; + case VectorOrigin::Target: + if (isBullet) + { + auto const pB = static_cast(pObject); + s.InitialOriginPos = pB->TargetCoords; + } + break; + } s.InitialLocation = GetPos(); if (isBullet) From e58d675611b1ccab2a475510fb6250563cc48a7b Mon Sep 17 00:00:00 2001 From: KenosisM Date: Sat, 27 Jun 2026 13:41:47 +0800 Subject: [PATCH 09/11] fix: Vector port from Kratos - B1-B8 bugfixes, M1-M10 feature parity, OriginNoUpdate auto-detect, default horizontal Origin circle plane, TargetOffset init fix --- src/Ext/Bullet/Hooks.cpp | 18 ++ src/New/Entity/VectorState.cpp | 267 ++++++++++++++++++++++--- src/New/Type/AttachEffectTypeClass.cpp | 36 ++++ src/New/Type/AttachEffectTypeClass.h | 40 +++- 4 files changed, 326 insertions(+), 35 deletions(-) diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index 1c9c04d6a7..397a521291 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -2,8 +2,21 @@ #include #include #include +#include #include +//static void VecLog(const char* fmt, ...) +//{ +// va_list args; +// va_start(args, fmt); +// FILE* f = fopen("vec_debug.log", "a"); +// if (f) { +// vfprintf(f, fmt, args); +// fclose(f); +// } +// va_end(args); +//} + // has everything inited except SpawnNextAnim at this point DEFINE_HOOK(0x466556, BulletClass_Init, 0x6) { @@ -201,6 +214,11 @@ DEFINE_HOOK(0x466781, BulletClass_AI_UpdateEnd, 0x6) if (pos.X != 0 || pos.Y != 0 || pos.Z != 0) { CoordStruct prev = pThis->GetCoords(); + //VecLog("[VEC] FRAME=%d APPLY_PREV=(%d,%d,%d) STORED=(%d,%d,%d) DELTA=(%d,%d,%d)\n", + // Unsorted::CurrentFrame, + // prev.X, prev.Y, prev.Z, + // pos.X, pos.Y, pos.Z, + // pos.X - prev.X, pos.Y - prev.Y, pos.Z - prev.Z); pThis->SetLocation(pos); pThis->SourceCoords = pos; if (pExt->VectorType->Vector_SyncFacing) diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp index db67aebb09..ff66cb5236 100644 --- a/src/New/Entity/VectorState.cpp +++ b/src/New/Entity/VectorState.cpp @@ -2,6 +2,19 @@ #include "AttachEffectClass.h" #include #include +#include + +static void VecLog(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + FILE* f = fopen("vec_debug.log", "a"); + if (f) { + vfprintf(f, fmt, args); + fclose(f); + } + va_end(args); +} static double V_Random(double min, double max) { @@ -33,6 +46,9 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (!pObject) return; + if (pObject->WhatAmI() == AbstractType::Building) + return; + if (s.DisabledTimer > 0) { s.DisabledTimer--; @@ -45,6 +61,17 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (skipFrame) return; + auto EffectiveOriginNoUpdate = [&]() -> bool { + if (pType->Vector_OriginNoUpdate.isset()) + return pType->Vector_OriginNoUpdate.Get(); + bool hasCircle = pType->Vector_CircleRadius > 0 || pType->Vector_CircleSpeed != 0 || pType->Vector_CircleAnglePerStep > 0.0 + || (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + || (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin); + bool hasTargetMode = pType->Vector_TargetFLH.isset() || pType->Vector_ReachTarget; + bool hasMoveTo = pType->Vector_MoveTo != CoordStruct::Empty; + return (hasTargetMode || hasMoveTo) && !hasCircle; + }; + s.MovementFrames++; s.NormalRotF += s.NormalStepF; s.NormalRotL += s.NormalStepL; @@ -71,22 +98,89 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat } s.InitialLocation = GetPos(); - if (isBullet) - { - auto const pB = static_cast(pObject); - double vx = pB->Velocity.X, vy = pB->Velocity.Y; - s.FacingRad = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + if (pType->Vector_NormalVector.isset() + || (pType->Vector_NormalRandomFMax > pType->Vector_NormalRandomFMin) + || (pType->Vector_NormalRandomLMax > pType->Vector_NormalRandomLMin) + || (pType->Vector_NormalRandomHMax > pType->Vector_NormalRandomHMin)) + { + CoordStruct nv = pType->Vector_NormalVector.isset() ? pType->Vector_NormalVector.Get() : CoordStruct{}; + if (pType->Vector_NormalRandomFMax > pType->Vector_NormalRandomFMin) + nv.X = V_Random(pType->Vector_NormalRandomFMin, pType->Vector_NormalRandomFMax); + if (pType->Vector_NormalRandomLMax > pType->Vector_NormalRandomLMin) + nv.Y = V_Random(pType->Vector_NormalRandomLMin, pType->Vector_NormalRandomLMax); + if (pType->Vector_NormalRandomHMax > pType->Vector_NormalRandomHMin) + nv.Z = V_Random(pType->Vector_NormalRandomHMin, pType->Vector_NormalRandomHMax); + double lenXY = std::sqrt((double)nv.X * nv.X + nv.Y * nv.Y); + s.FacingRad = lenXY > 1e-6 ? std::atan2(nv.X, nv.Y) : 0.0; + s.TiltRad = lenXY > 1e-6 ? std::atan2(nv.Z, lenXY) : (nv.Z > 0 ? 3.1415926535 / 2.0 : -3.1415926535 / 2.0); } else { - auto const pT = static_cast(pObject); - s.FacingRad = pT->PrimaryFacing.Current().GetRadian<32>(); + switch (pType->Vector_Origin) + { + case VectorOrigin::Launcher: + if (pInvoker) + { + auto const pLT = static_cast(pInvoker); + s.FacingRad = pLT->TurretFacing().GetRadian<32>(); + } + break; + case VectorOrigin::Target: + { + CoordStruct selfPos = GetPos(); + CoordStruct targetPos = selfPos; + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) + targetPos = pB->Target->GetCoords(); + else + targetPos = pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) + targetPos = pT->Target->GetCoords(); + } + double dx = selfPos.X - targetPos.X; + double dy = selfPos.Y - targetPos.Y; + double dz = selfPos.Z - targetPos.Z; + s.FacingRad = std::atan2(dy, dx); + double lenXY = std::sqrt(dx * dx + dy * dy); + s.TiltRad = (lenXY > 1e-6 && pType->Vector_AllowedTilt) ? std::atan2(dz, lenXY) : 0.0; + break; + } + default: + if (isBullet) + { + auto const pB = static_cast(pObject); + double vx = pB->Velocity.X, vy = pB->Velocity.Y; + s.FacingRad = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + } + else + { + auto const pT = static_cast(pObject); + s.FacingRad = pT->TurretFacing().GetRadian<32>(); + } + s.TiltRad = 0.0; + break; + } } - s.TiltRad = 0.0; s.OriginFacing = s.FacingRad; - s.OriginTilt = 0.0; + s.OriginTilt = s.TiltRad; - double speed = static_cast(pType->Vector_InitialSpeed >= 0 ? pType->Vector_InitialSpeed : 0); + double speed; + if (pType->Vector_InitialSpeed >= 0) + speed = static_cast(pType->Vector_InitialSpeed); + else if (isBullet) + speed = static_cast(static_cast(pObject)->Speed); + else + { + auto const pT = static_cast(pObject); + TechnoTypeClass* pTT = pT->GetTechnoType(); + speed = pTT->JumpjetSpeed > 0 ? static_cast(pTT->JumpjetSpeed) : static_cast(pTT->Speed); + } if (pType->Vector_RandomSpeedMin != pType->Vector_RandomSpeedMax) speed = V_Random(pType->Vector_RandomSpeedMin, pType->Vector_RandomSpeedMax); s.CurrentSpeed = speed; @@ -108,7 +202,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (pType->Vector_TargetFLH.isset()) { - CoordStruct offset = pType->Vector_TargetFLH.Get(); + CoordStruct offset = CoordStruct::Empty; if (pType->Vector_TargetOffsetFMin != pType->Vector_TargetOffsetFMax) offset.X = pType->Vector_TargetOffsetFMin + rand() % (pType->Vector_TargetOffsetFMax - pType->Vector_TargetOffsetFMin + 1); if (pType->Vector_TargetOffsetLMin != pType->Vector_TargetOffsetLMax) @@ -162,8 +256,11 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat effectiveTilt = 0.0; } - bool hasNormal = pType->Vector_NormalVector.isset(); - if (!pType->Vector_OriginNoUpdate && !hasNormal && !pType->Vector_OriginIsOnWorld) + bool hasNormal = pType->Vector_NormalVector.isset() + || (pType->Vector_NormalRandomFMax > pType->Vector_NormalRandomFMin) + || (pType->Vector_NormalRandomLMax > pType->Vector_NormalRandomLMin) + || (pType->Vector_NormalRandomHMax > pType->Vector_NormalRandomHMin); + if (! EffectiveOriginNoUpdate() && !hasNormal && !pType->Vector_OriginIsOnWorld) { switch (pType->Vector_Origin) { @@ -191,7 +288,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (pB->Target) targetPos = pB->Target->GetCoords(); else - break; + targetPos = pB->TargetCoords; } else { @@ -231,7 +328,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat } break; case VectorOrigin::Launcher: - if (pInvoker) + if (pInvoker && pInvoker->IsAlive) { auto const pLT = static_cast(pInvoker); mainFacingDir = pType->Vector_OriginIsOnBody @@ -249,7 +346,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat switch (pType->Vector_Origin) { case VectorOrigin::Target: - if (pType->Vector_OriginNoUpdate) + if ( EffectiveOriginNoUpdate()) originPos = s.InitialOriginPos; else if (isBullet) { @@ -263,15 +360,15 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat } break; case VectorOrigin::Launcher: - originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + originPos = EffectiveOriginNoUpdate() ? s.InitialOriginPos : (pInvoker ? pInvoker->GetCoords() : GetPos()); break; case VectorOrigin::Source: - originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + originPos = EffectiveOriginNoUpdate() ? s.InitialOriginPos : (pInvoker ? pInvoker->GetCoords() : GetPos()); break; case VectorOrigin::Self: - originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : GetPos(); + originPos = EffectiveOriginNoUpdate() ? s.InitialOriginPos : GetPos(); break; } @@ -285,7 +382,8 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat // ================================================================ bool hasCircle = pType->Vector_CircleRadius > 0 || pType->Vector_CircleSpeed != 0 || pType->Vector_CircleAnglePerStep > 0.0 || (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) - || (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin); + || (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin) + || (pType->Vector_CircleRandomAngleMax2 > pType->Vector_CircleRandomAngleMin2); if (hasCircle) { @@ -302,7 +400,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat s.CurrentCircleSpeed = static_cast(pType->Vector_CircleSpeed); s.CurrentCircleRadius = calcRadius; if (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) - s.CurrentCircleRadius = static_cast(rand() % (pType->Vector_CircleRandomRadiusMax - pType->Vector_CircleRandomRadiusMin + 1) + pType->Vector_CircleRandomRadiusMin); + s.CurrentCircleRadius = V_Random(static_cast(pType->Vector_CircleRandomRadiusMin), static_cast(pType->Vector_CircleRandomRadiusMax)); } s.CurrentCircleSpeed += pType->Vector_CircleSpeedAcceleration; if (pType->Vector_CircleMaxSpeed != 0 && s.CurrentCircleSpeed > pType->Vector_CircleMaxSpeed) @@ -314,7 +412,14 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat { s.CurrentCircleAngle = pType->Vector_CircleAnglePerStep; if (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin) - s.CurrentCircleAngle = V_Random(pType->Vector_CircleRandomAngleMin, pType->Vector_CircleRandomAngleMax); + { + bool useSecond = pType->Vector_CircleRandomAngleMax2 > pType->Vector_CircleRandomAngleMin2 + && (std::rand() % 2); + if (useSecond) + s.CurrentCircleAngle = V_Random(pType->Vector_CircleRandomAngleMin2, pType->Vector_CircleRandomAngleMax2); + else + s.CurrentCircleAngle = V_Random(pType->Vector_CircleRandomAngleMin, pType->Vector_CircleRandomAngleMax); + } } s.CurrentCircleAngle += pType->Vector_CircleAngleAcceleration; if (pType->Vector_CircleMaxAngle != 0 && s.CurrentCircleAngle > pType->Vector_CircleMaxAngle) @@ -344,6 +449,10 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (hasOriginSub) { + VecLog("[ORIGIN] Frame=%d MOVFRM=%d CR=%d CSP=%d CAng=%.1f\n", + Unsorted::CurrentFrame, s.MovementFrames, + (int)pType->Vector_OriginCircleRadius, (int)pType->Vector_OriginCircleSpeed, + pType->Vector_OriginCircleAnglePerStep); CoordStruct baseCenter = originPos; if (pType->Vector_OriginOrigin != VectorOrigin::Self) @@ -384,20 +493,44 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat baseCenter.Z += pType->Vector_OriginOriginFLH.Get().Z; } + if (pType->Vector_OriginCircleOffset.isset()) + { + baseCenter.X += pType->Vector_OriginCircleOffset.Get().X; + baseCenter.Y += pType->Vector_OriginCircleOffset.Get().Y; + baseCenter.Z += pType->Vector_OriginCircleOffset.Get().Z; + } + if (s.MovementFrames == 1) { s.OriginOffset = { circleCenter.X - baseCenter.X, circleCenter.Y - baseCenter.Y, circleCenter.Z - baseCenter.Z }; s.OriginCircleRadiusRuntime = static_cast(pType->Vector_OriginCircleRadius); s.OriginCircleSpeedRuntime = static_cast(pType->Vector_OriginCircleSpeed); s.OriginCircleAngleRuntime = 0.0; - s.OriginTargetOffset = CoordStruct::Empty; + { + int ofx = pType->Vector_OriginTargetOffsetFMax > pType->Vector_OriginTargetOffsetFMin + ? static_cast(V_Random(static_cast(pType->Vector_OriginTargetOffsetFMin), static_cast(pType->Vector_OriginTargetOffsetFMax))) : 0; + int ofy = pType->Vector_OriginTargetOffsetLMax > pType->Vector_OriginTargetOffsetLMin + ? static_cast(V_Random(static_cast(pType->Vector_OriginTargetOffsetLMin), static_cast(pType->Vector_OriginTargetOffsetLMax))) : 0; + int ofz = pType->Vector_OriginTargetOffsetHMax > pType->Vector_OriginTargetOffsetHMin + ? static_cast(V_Random(static_cast(pType->Vector_OriginTargetOffsetHMin), static_cast(pType->Vector_OriginTargetOffsetHMax))) : 0; + s.OriginTargetOffset = { ofx, ofy, ofz }; + } if (pType->Vector_OriginNormalVector.isset()) { CoordStruct nv = pType->Vector_OriginNormalVector.Get(); double len = std::sqrt(static_cast(nv.X * nv.X + nv.Y * nv.Y)); - s.OriginFacing = len > 1e-6 ? std::atan2(static_cast(nv.Y), static_cast(nv.X)) : 0; + s.OriginFacing = len > 1e-6 ? std::atan2(static_cast(nv.X), static_cast(nv.Y)) : 0; s.OriginTilt = len > 1e-6 ? std::atan2(static_cast(nv.Z), len) : (nv.Z > 0 ? 3.14159265358979323846 / 2.0 : -3.14159265358979323846 / 2.0); + // 哨兵:NormalVector 存在但未显式设 CircleRadius 时标记为 0 + if (pType->Vector_OriginCircleRadius < 0) + s.OriginCircleRadiusRuntime = 0.0; + } + else + { + // 默认水平圆面(法向量朝上) + s.OriginFacing = 0; + s.OriginTilt = 3.14159265358979323846 / 2.0; } } @@ -429,12 +562,54 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat s.OriginSpeed = pType->Vector_OriginInitialSpeed >= 0 ? pType->Vector_OriginInitialSpeed : 40.0; CoordStruct targetWorld = AttachEffectClass::GetFLHAbsoluteCoords(baseCenter, pType->Vector_OriginTargetFLH.Get() + s.OriginTargetOffset, V_Radians2Dir(oFacing)); - int dx = targetWorld.X - originCenter.X, dy = targetWorld.Y - originCenter.Y, dz = targetWorld.Z - originCenter.Z; - double dist = std::sqrt(static_cast(dx * dx + dy * dy + dz * dz)); - if (dist >= 1.0) + + if (pType->Vector_OriginReachTarget) { - double sv = s.OriginSpeed / dist; - disp = { static_cast(dx * sv), static_cast(dy * sv), static_cast(dz * sv) }; + int totalDuration = pType->Duration / pType->Vector_TimeStep; + if (totalDuration < 1) totalDuration = 1; + int effectiveDuration = totalDuration - pType->Vector_DisabledFrames; + if (effectiveDuration < 1) effectiveDuration = 1; + int effectiveSteps = effectiveDuration; + int rem = effectiveSteps - s.MovementFrames; + if (rem <= 0) + { + disp.X = targetWorld.X - originCenter.X; + disp.Y = targetWorld.Y - originCenter.Y; + disp.Z = targetWorld.Z - originCenter.Z; + s.OriginOffset.X += disp.X; s.OriginOffset.Y += disp.Y; s.OriginOffset.Z += disp.Z; + circleCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + s.PrevCircleCenter = circleCenter; + s.DisabledTimer = -1; // Deactivate-equivalent + s.OriginElapsed = 0; // no further OriginUpdate + } + else + { + disp.X = (targetWorld.X - originCenter.X) / rem; + disp.Y = (targetWorld.Y - originCenter.Y) / rem; + disp.Z = (targetWorld.Z - originCenter.Z) / rem; + if (pType->Vector_OriginArcHeight) + { + double t = effectiveSteps > 0 ? static_cast(s.OriginElapsed) / effectiveSteps : 0; + double t0 = effectiveSteps > 0 ? static_cast(s.OriginElapsed - 1) / effectiveSteps : 0; + disp.Z += static_cast(4.0 * pType->Vector_OriginArcHeight * (t * (1 - t) - t0 * (1 - t0))); + } + } + } + else + { + s.OriginSpeed += static_cast(pType->Vector_OriginAcceleration); + if (pType->Vector_OriginMaxSpeed >= 0 && s.OriginSpeed > pType->Vector_OriginMaxSpeed) + s.OriginSpeed = static_cast(pType->Vector_OriginMaxSpeed); + if (pType->Vector_OriginMinSpeed >= 0 && s.OriginSpeed < pType->Vector_OriginMinSpeed) + s.OriginSpeed = static_cast(pType->Vector_OriginMinSpeed); + + int dx = targetWorld.X - originCenter.X, dy = targetWorld.Y - originCenter.Y, dz = targetWorld.Z - originCenter.Z; + double dist = std::sqrt(static_cast(dx * dx + dy * dy + dz * dz)); + if (dist >= 1.0) + { + double sv = s.OriginSpeed / dist; + disp = { static_cast(dx * sv), static_cast(dy * sv), static_cast(dz * sv) }; + } } } else @@ -447,7 +622,7 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (pType->Vector_OriginCircleSpeed != 0 && tr > 0) stepO = V_Rad2Deg(pType->Vector_OriginCircleSpeed / tr); s.OriginCircleAngleRuntime += stepO; - double r = V_Deg2Rad(stepO); + double r = pType->Vector_OriginLissajous ? V_Deg2Rad(s.OriginCircleAngleRuntime) : V_Deg2Rad(stepO); double ca = std::cos(r), sa = std::sin(r); double dxO = static_cast(s.OriginOffset.X); double dyO = static_cast(s.OriginOffset.Y); @@ -471,6 +646,11 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat s.OriginOffset.X += disp.X; s.OriginOffset.Y += disp.Y; s.OriginOffset.Z += disp.Z; circleCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + VecLog("[ORIGIN] of=%.3f ot=%.3f rad=%.1f ang=%.1f disp=(%d,%d,%d) offset=(%d,%d,%d) center=(%d,%d,%d)\n", + oFacing, oTilt, s.OriginCircleRadiusRuntime, s.OriginCircleAngleRuntime, + disp.X, disp.Y, disp.Z, + s.OriginOffset.X, s.OriginOffset.Y, s.OriginOffset.Z, + circleCenter.X, circleCenter.Y, circleCenter.Z); s.OriginElapsed++; } @@ -631,6 +811,14 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat CoordStruct frameTargetFlh = { pType->Vector_TargetFLH.Get().X + s.TargetOffset.X, pType->Vector_TargetFLH.Get().Y + s.TargetOffset.Y, pType->Vector_TargetFLH.Get().Z + s.TargetOffset.Z }; + //VecLog("[VEC] Frame=%d FLH=(%d,%d,%d) TGTFLH=(%d,%d,%d) OFFSET=(%d,%d,%d) ORIGIN=%d ONWORLD=%d ONBODY=%d\n", + // Unsorted::CurrentFrame, + // pType->Vector_TargetFLH.Get().X, pType->Vector_TargetFLH.Get().Y, pType->Vector_TargetFLH.Get().Z, + // frameTargetFlh.X, frameTargetFlh.Y, frameTargetFlh.Z, + // s.TargetOffset.X, s.TargetOffset.Y, s.TargetOffset.Z, + // static_cast(pType->Vector_Origin.Get()), + // static_cast(pType->Vector_OriginIsOnWorld.Get()), + // static_cast(pType->Vector_OriginIsOnBody.Get())); CoordStruct frameTarget; switch (pType->Vector_Origin) @@ -658,19 +846,28 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat double dirLen = std::sqrt(static_cast(dirVec.X * dirVec.X + dirVec.Y * dirVec.Y + dirVec.Z * dirVec.Z)); CoordStruct resultDisp{ 0, 0, 0 }; + //VecLog("[VEC] CURR=(%d,%d,%d) FDIR=%.4f FACING=(%.4f,%.4f) FRMTGT=(%d,%d,%d) DIRVEC=(%d,%d,%d) DIRLEN=%.1f MOVFRM=%d\n", + // currentPos.X, currentPos.Y, currentPos.Z, + // mainFacingDir.GetRadian<32>(), + // effectiveFacing, effectiveTilt, + // frameTarget.X, frameTarget.Y, frameTarget.Z, + // dirVec.X, dirVec.Y, dirVec.Z, + // dirLen, + // s.MovementFrames); + if (pType->Vector_ReachTarget) { int totalDuration = pType->Duration / pType->Vector_TimeStep; if (totalDuration < 1) totalDuration = 1; int effectiveDuration = totalDuration - pType->Vector_DisabledFrames; if (effectiveDuration < 1) effectiveDuration = 1; - int remainingFrames = effectiveDuration - s.MovementFrames; + int remainingFrames = effectiveDuration - s.MovementFrames + 1; if (pType->Vector_ReachTargetEarlyEnd > 0 && pType->Vector_ReachTargetEarlyEnd < effectiveDuration && remainingFrames <= pType->Vector_ReachTargetEarlyEnd) { - s.DisabledTimer = pType->Vector_ReachTargetEarlyEnd; - return; + s.DisabledTimer = -1; + s.MovementFrames = effectiveDuration + 1; // 退出 EarlyEnd 区间,下一步 remainingFrames <= 0 } if (remainingFrames <= 0) @@ -762,6 +959,10 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat { auto const pB = static_cast(pObject); s.StoredDisp = { currentPos.X + resultDisp.X, currentPos.Y + resultDisp.Y, currentPos.Z + resultDisp.Z }; + //VecLog("[VEC] RESULT DISP=(%d,%d,%d) STORED=(%d,%d,%d) VEL=(%.1f,%.1f,%.1f)\n", + // resultDisp.X, resultDisp.Y, resultDisp.Z, + // s.StoredDisp.X, s.StoredDisp.Y, s.StoredDisp.Z, + // pB->Velocity.X, pB->Velocity.Y, pB->Velocity.Z); } else { diff --git a/src/New/Type/AttachEffectTypeClass.cpp b/src/New/Type/AttachEffectTypeClass.cpp index 8c4301a84c..669d17af9b 100644 --- a/src/New/Type/AttachEffectTypeClass.cpp +++ b/src/New/Type/AttachEffectTypeClass.cpp @@ -198,6 +198,12 @@ void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) this->Vector_NormalRandomF.Read(exINI, pSection, "Vector.NormalRandomF"); this->Vector_NormalRandomL.Read(exINI, pSection, "Vector.NormalRandomL"); this->Vector_NormalRandomH.Read(exINI, pSection, "Vector.NormalRandomH"); + this->Vector_NormalRandomFMin.Read(exINI, pSection, "Vector.NormalRandomF.Min"); + this->Vector_NormalRandomFMax.Read(exINI, pSection, "Vector.NormalRandomF.Max"); + this->Vector_NormalRandomLMin.Read(exINI, pSection, "Vector.NormalRandomL.Min"); + this->Vector_NormalRandomLMax.Read(exINI, pSection, "Vector.NormalRandomL.Max"); + this->Vector_NormalRandomHMin.Read(exINI, pSection, "Vector.NormalRandomH.Min"); + this->Vector_NormalRandomHMax.Read(exINI, pSection, "Vector.NormalRandomH.Max"); this->Vector_NormalFAnglePerStep.Read(exINI, pSection, "Vector.NormalFAnglePerStep"); this->Vector_NormalLAnglePerStep.Read(exINI, pSection, "Vector.NormalLAnglePerStep"); this->Vector_NormalHAnglePerStep.Read(exINI, pSection, "Vector.NormalHAnglePerStep"); @@ -239,6 +245,8 @@ void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) this->Vector_CircleRandomRadiusMax.Read(exINI, pSection, "Vector.CircleRandomRadius.Max"); this->Vector_CircleRandomAngleMin.Read(exINI, pSection, "Vector.CircleRandomAngle.Min"); this->Vector_CircleRandomAngleMax.Read(exINI, pSection, "Vector.CircleRandomAngle.Max"); + this->Vector_CircleRandomAngleMin2.Read(exINI, pSection, "Vector.CircleRandomAngle.Min2"); + this->Vector_CircleRandomAngleMax2.Read(exINI, pSection, "Vector.CircleRandomAngle.Max2"); this->Vector_CircleMaxAngle.Read(exINI, pSection, "Vector.CircleMaxAngle"); this->Vector_CircleMinAngle.Read(exINI, pSection, "Vector.CircleMinAngle"); this->Vector_TargetOffsetFMin.Read(exINI, pSection, "Vector.TargetOffsetF.Min"); @@ -257,11 +265,21 @@ void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) this->Vector_OriginGrowRate.Read(exINI, pSection, "Vector.Origin.GrowRate"); this->Vector_OriginTargetFLH.Read(exINI, pSection, "Vector.Origin.TargetFLH"); this->Vector_OriginInitialSpeed.Read(exINI, pSection, "Vector.Origin.InitialSpeed"); + this->Vector_OriginAcceleration.Read(exINI, pSection, "Vector.Origin.Acceleration"); + this->Vector_OriginMaxSpeed.Read(exINI, pSection, "Vector.Origin.MaxSpeed"); + this->Vector_OriginMinSpeed.Read(exINI, pSection, "Vector.Origin.MinSpeed"); + this->Vector_OriginTargetOffsetFMin.Read(exINI, pSection, "Vector.Origin.TargetOffsetF.Min"); + this->Vector_OriginTargetOffsetFMax.Read(exINI, pSection, "Vector.Origin.TargetOffsetF.Max"); + this->Vector_OriginTargetOffsetLMin.Read(exINI, pSection, "Vector.Origin.TargetOffsetL.Min"); + this->Vector_OriginTargetOffsetLMax.Read(exINI, pSection, "Vector.Origin.TargetOffsetL.Max"); + this->Vector_OriginTargetOffsetHMin.Read(exINI, pSection, "Vector.Origin.TargetOffsetH.Min"); + this->Vector_OriginTargetOffsetHMax.Read(exINI, pSection, "Vector.Origin.TargetOffsetH.Max"); this->Vector_OriginReachTarget.Read(exINI, pSection, "Vector.Origin.ReachTarget"); this->Vector_OriginArcHeight.Read(exINI, pSection, "Vector.Origin.ArcHeight"); this->Vector_OriginCircleRadius.Read(exINI, pSection, "Vector.Origin.CircleRadius"); this->Vector_OriginCircleSpeed.Read(exINI, pSection, "Vector.Origin.CircleSpeed"); this->Vector_OriginCircleAnglePerStep.Read(exINI, pSection, "Vector.Origin.CircleAnglePerStep"); + this->Vector_OriginLissajous.Read(exINI, pSection, "Vector.Origin.Lissajous"); this->Vector_OriginCircleRadiusGrow.Read(exINI, pSection, "Vector.Origin.CircleRadiusGrow"); this->Vector_OriginCircleMaxRadius.Read(exINI, pSection, "Vector.Origin.CircleMaxRadius"); this->Vector_OriginCircleMinRadius.Read(exINI, pSection, "Vector.Origin.CircleMinRadius"); @@ -385,6 +403,12 @@ void AttachEffectTypeClass::Serialize(T& Stm) .Process(this->Vector_NormalRandomF) .Process(this->Vector_NormalRandomL) .Process(this->Vector_NormalRandomH) + .Process(this->Vector_NormalRandomFMin) + .Process(this->Vector_NormalRandomFMax) + .Process(this->Vector_NormalRandomLMin) + .Process(this->Vector_NormalRandomLMax) + .Process(this->Vector_NormalRandomHMin) + .Process(this->Vector_NormalRandomHMax) .Process(this->Vector_NormalFAnglePerStep) .Process(this->Vector_NormalLAnglePerStep) .Process(this->Vector_NormalHAnglePerStep) @@ -426,6 +450,8 @@ void AttachEffectTypeClass::Serialize(T& Stm) .Process(this->Vector_CircleRandomRadiusMax) .Process(this->Vector_CircleRandomAngleMin) .Process(this->Vector_CircleRandomAngleMax) + .Process(this->Vector_CircleRandomAngleMin2) + .Process(this->Vector_CircleRandomAngleMax2) .Process(this->Vector_CircleMaxAngle) .Process(this->Vector_CircleMinAngle) .Process(this->Vector_TargetOffsetFMin) @@ -444,11 +470,21 @@ void AttachEffectTypeClass::Serialize(T& Stm) .Process(this->Vector_OriginGrowRate) .Process(this->Vector_OriginTargetFLH) .Process(this->Vector_OriginInitialSpeed) + .Process(this->Vector_OriginAcceleration) + .Process(this->Vector_OriginMaxSpeed) + .Process(this->Vector_OriginMinSpeed) + .Process(this->Vector_OriginTargetOffsetFMin) + .Process(this->Vector_OriginTargetOffsetFMax) + .Process(this->Vector_OriginTargetOffsetLMin) + .Process(this->Vector_OriginTargetOffsetLMax) + .Process(this->Vector_OriginTargetOffsetHMin) + .Process(this->Vector_OriginTargetOffsetHMax) .Process(this->Vector_OriginReachTarget) .Process(this->Vector_OriginArcHeight) .Process(this->Vector_OriginCircleRadius) .Process(this->Vector_OriginCircleSpeed) .Process(this->Vector_OriginCircleAnglePerStep) + .Process(this->Vector_OriginLissajous) .Process(this->Vector_OriginCircleRadiusGrow) .Process(this->Vector_OriginCircleMaxRadius) .Process(this->Vector_OriginCircleMinRadius) diff --git a/src/New/Type/AttachEffectTypeClass.h b/src/New/Type/AttachEffectTypeClass.h index 727c2ea05c..bf1dfead11 100644 --- a/src/New/Type/AttachEffectTypeClass.h +++ b/src/New/Type/AttachEffectTypeClass.h @@ -118,7 +118,7 @@ class AttachEffectTypeClass final : public Enumerable Valueable Vector_OriginIsOnBody; Valueable Vector_Origin; Nullable Vector_OriginFLH; - Valueable Vector_OriginNoUpdate; + Nullable Vector_OriginNoUpdate; Valueable Vector_Force; Valueable Vector_Freeze; Valueable Vector_AllowedTilt; @@ -126,6 +126,12 @@ class AttachEffectTypeClass final : public Enumerable Valueable Vector_NormalRandomF; Valueable Vector_NormalRandomL; Valueable Vector_NormalRandomH; + Valueable Vector_NormalRandomFMin; + Valueable Vector_NormalRandomFMax; + Valueable Vector_NormalRandomLMin; + Valueable Vector_NormalRandomLMax; + Valueable Vector_NormalRandomHMin; + Valueable Vector_NormalRandomHMax; Valueable Vector_NormalFAnglePerStep; Valueable Vector_NormalLAnglePerStep; Valueable Vector_NormalHAnglePerStep; @@ -167,6 +173,8 @@ class AttachEffectTypeClass final : public Enumerable Valueable Vector_CircleRandomRadiusMax; Valueable Vector_CircleRandomAngleMin; Valueable Vector_CircleRandomAngleMax; + Valueable Vector_CircleRandomAngleMin2; + Valueable Vector_CircleRandomAngleMax2; Valueable Vector_CircleMaxAngle; Valueable Vector_CircleMinAngle; Valueable Vector_TargetOffsetFMin; @@ -185,11 +193,21 @@ class AttachEffectTypeClass final : public Enumerable Nullable Vector_OriginGrowRate; Nullable Vector_OriginTargetFLH; Valueable Vector_OriginInitialSpeed; + Valueable Vector_OriginAcceleration; + Valueable Vector_OriginMaxSpeed; + Valueable Vector_OriginMinSpeed; + Valueable Vector_OriginTargetOffsetFMin; + Valueable Vector_OriginTargetOffsetFMax; + Valueable Vector_OriginTargetOffsetLMin; + Valueable Vector_OriginTargetOffsetLMax; + Valueable Vector_OriginTargetOffsetHMin; + Valueable Vector_OriginTargetOffsetHMax; Valueable Vector_OriginReachTarget; Valueable Vector_OriginArcHeight; Valueable Vector_OriginCircleRadius; Valueable Vector_OriginCircleSpeed; Valueable Vector_OriginCircleAnglePerStep; + Valueable Vector_OriginLissajous; Valueable Vector_OriginCircleRadiusGrow; Valueable Vector_OriginCircleMaxRadius; Valueable Vector_OriginCircleMinRadius; @@ -299,7 +317,7 @@ class AttachEffectTypeClass final : public Enumerable , Vector_OriginIsOnBody { false } , Vector_Origin { VectorOrigin::Self } , Vector_OriginFLH {} - , Vector_OriginNoUpdate { false } + , Vector_OriginNoUpdate {} , Vector_Force { true } , Vector_Freeze { false } , Vector_AllowedTilt { false } @@ -307,6 +325,12 @@ class AttachEffectTypeClass final : public Enumerable , Vector_NormalRandomF { 0.0 } , Vector_NormalRandomL { 0.0 } , Vector_NormalRandomH { 0.0 } + , Vector_NormalRandomFMin { 0.0 } + , Vector_NormalRandomFMax { 0.0 } + , Vector_NormalRandomLMin { 0.0 } + , Vector_NormalRandomLMax { 0.0 } + , Vector_NormalRandomHMin { 0.0 } + , Vector_NormalRandomHMax { 0.0 } , Vector_NormalFAnglePerStep { 0.0 } , Vector_NormalLAnglePerStep { 0.0 } , Vector_NormalHAnglePerStep { 0.0 } @@ -348,6 +372,8 @@ class AttachEffectTypeClass final : public Enumerable , Vector_CircleRandomRadiusMax { -1 } , Vector_CircleRandomAngleMin { -1 } , Vector_CircleRandomAngleMax { -1 } + , Vector_CircleRandomAngleMin2 { -1 } + , Vector_CircleRandomAngleMax2 { -1 } , Vector_CircleMaxAngle { 0.0 } , Vector_CircleMinAngle { 0.0 } , Vector_TargetOffsetFMin { 0 } @@ -366,11 +392,21 @@ class AttachEffectTypeClass final : public Enumerable , Vector_OriginGrowRate {} , Vector_OriginTargetFLH {} , Vector_OriginInitialSpeed { -1 } + , Vector_OriginAcceleration { 0 } + , Vector_OriginMaxSpeed { -1 } + , Vector_OriginMinSpeed { -1 } + , Vector_OriginTargetOffsetFMin { 0 } + , Vector_OriginTargetOffsetFMax { 0 } + , Vector_OriginTargetOffsetLMin { 0 } + , Vector_OriginTargetOffsetLMax { 0 } + , Vector_OriginTargetOffsetHMin { 0 } + , Vector_OriginTargetOffsetHMax { 0 } , Vector_OriginReachTarget { false } , Vector_OriginArcHeight { 0 } , Vector_OriginCircleRadius { -1 } , Vector_OriginCircleSpeed { 0 } , Vector_OriginCircleAnglePerStep { 0.0 } + , Vector_OriginLissajous { false } , Vector_OriginCircleRadiusGrow { 0 } , Vector_OriginCircleMaxRadius { 0 } , Vector_OriginCircleMinRadius { 0 } From 1accd6104627004cd646efab350cfc6284ae04bf Mon Sep 17 00:00:00 2001 From: KenosisM Date: Sat, 27 Jun 2026 13:43:05 +0800 Subject: [PATCH 10/11] chore: comment out VecLog debug logging --- .codegraph/.gitignore | 5 + VectorPort.rar | Bin 0 -> 56514 bytes VectorPort/Phobos.props | 129 + VectorPort/Phobos.vcxproj | 418 +++ VectorPort/src/Ext/Bullet/Body.cpp | 603 +++++ VectorPort/src/Ext/Bullet/Body.h | 104 + VectorPort/src/Ext/Bullet/Hooks.cpp | 627 +++++ VectorPort/src/Ext/Techno/Body.Update.cpp | 2278 +++++++++++++++++ .../src/New/Entity/AttachEffectClass.cpp | 1165 +++++++++ VectorPort/src/New/Entity/AttachEffectClass.h | 149 ++ VectorPort/src/New/Entity/VectorState.cpp | 752 ++++++ VectorPort/src/New/Entity/VectorState.h | 94 + .../src/New/Type/AttachEffectTypeClass.cpp | 716 ++++++ .../src/New/Type/AttachEffectTypeClass.h | 529 ++++ Vector_FixList.md | 153 ++ phobosvector.ini | 127 + src/New/Entity/VectorState.cpp | 40 +- src/New/Entity/VectorState.cpp.circlebackup | 757 ++++++ 18 files changed, 8626 insertions(+), 20 deletions(-) create mode 100644 .codegraph/.gitignore create mode 100644 VectorPort.rar create mode 100644 VectorPort/Phobos.props create mode 100644 VectorPort/Phobos.vcxproj create mode 100644 VectorPort/src/Ext/Bullet/Body.cpp create mode 100644 VectorPort/src/Ext/Bullet/Body.h create mode 100644 VectorPort/src/Ext/Bullet/Hooks.cpp create mode 100644 VectorPort/src/Ext/Techno/Body.Update.cpp create mode 100644 VectorPort/src/New/Entity/AttachEffectClass.cpp create mode 100644 VectorPort/src/New/Entity/AttachEffectClass.h create mode 100644 VectorPort/src/New/Entity/VectorState.cpp create mode 100644 VectorPort/src/New/Entity/VectorState.h create mode 100644 VectorPort/src/New/Type/AttachEffectTypeClass.cpp create mode 100644 VectorPort/src/New/Type/AttachEffectTypeClass.h create mode 100644 Vector_FixList.md create mode 100644 phobosvector.ini create mode 100644 src/New/Entity/VectorState.cpp.circlebackup diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 0000000000..d20c0fe4bd --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,5 @@ +# CodeGraph data files — local to each machine, not for committing. +# Ignore everything in .codegraph/ except this file itself, so transient +# files (the database, daemon.pid, sockets, logs) never show up in git. +* +!.gitignore diff --git a/VectorPort.rar b/VectorPort.rar new file mode 100644 index 0000000000000000000000000000000000000000..d07db5c96dfc3dec26b994b6ee8a7539cc4b2125 GIT binary patch literal 56514 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 z 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 VK%tD*|3-@urH_s}A~F)-KL9`QfD!-z literal 0 HcmV?d00001 diff --git a/VectorPort/Phobos.props b/VectorPort/Phobos.props new file mode 100644 index 0000000000..e9d93cb259 --- /dev/null +++ b/VectorPort/Phobos.props @@ -0,0 +1,129 @@ + + + + + Debug + Win32 + + + DevBuild + Win32 + + + Release + Win32 + + + + + DynamicLibrary + false + v145 + NotSet + + + + + false + $(ExpandedIncludePath);$(MSBuildThisFileDirectory)src\;$(YRppDir);$(VC_IncludePath) + false + $(Configuration)\ + $(Configuration)\IntDir\ + + dbghelp.lib;onecore.lib + + + + + Level4 + true + + true + SYR_VER=2;HAS_EXCEPTIONS=0;NOMINMAX;_CRT_SECURE_NO_WARNINGS;_WIN32_WINNT=0x0601;NTDDI_VERSION=0x06010000;%(PreprocessorDefinitions);%(AdditionalDefinitions);PHOBOS_DLL="$(ProjectName).dll" + true + false + MultiThreaded + 8Bytes + false + false + StreamingSIMDExtensions2 + true + false + stdcpp20 + $(IntDir)\%(Directory) + $(IntDir)\%(Directory) + StdCall + 4100;4201;4530;4731;4740;4458;4819;5103;5105 + true + true + true + + + true + true + $(PhobosExtraLibs);%(AdditionalDependencies) + Windows + $(OutDir)$(TargetName).pdb + $(IntDir)$(TargetName).pgd + $(IntDir)$(TargetName).lib + + + + + + IS_RELEASE_VER;%(PreprocessorDefinitions) + MaxSpeed + true + + + + + + + IS_RELEASE_VER;%(PreprocessorDefinitions) + + + + + + MaxSpeed + true + + + + + + + + + + DEBUG;%(PreprocessorDefinitions) + Disabled + false + 4100;4201;4530;4731;4740;4458;4819;5103;5105;26495 + + + DEBUG;%(PreprocessorDefinitions) + + + + + + /DGIT_COMMIT="$(GitCommit)" %(AdditionalOptions) + + + /DGIT_COMMIT="$(GitCommit)" %(AdditionalOptions) + + + + + + /DGIT_BRANCH="$(GitBranch)" %(AdditionalOptions) + + + /DGIT_BRANCH="$(GitBranch)" %(AdditionalOptions) + + + + + diff --git a/VectorPort/Phobos.vcxproj b/VectorPort/Phobos.vcxproj new file mode 100644 index 0000000000..35045b0e99 --- /dev/null +++ b/VectorPort/Phobos.vcxproj @@ -0,0 +1,418 @@ + + + + true + + + Phobos + Phobos + {3FAF7126-F38C-4D1E-9973-C21A37870F60} + 15.0 + 10.0 + true + $(MSBuildThisFileDirectory) + $(ThisDir)YRpp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/VectorPort/src/Ext/Bullet/Body.cpp b/VectorPort/src/Ext/Bullet/Body.cpp new file mode 100644 index 0000000000..e99b178c77 --- /dev/null +++ b/VectorPort/src/Ext/Bullet/Body.cpp @@ -0,0 +1,603 @@ +#include "Body.h" + +#include +#include +#include +#include +#include +#include + +BulletExt::ExtContainer BulletExt::ExtMap; + +void BulletExt::ExtData::InterceptBullet(TechnoClass* pSource, BulletClass* pInterceptor) +{ + const auto pThis = this->OwnerObject(); + auto pTypeExt = this->TypeExtData; + const auto pInterceptorType = BulletExt::ExtMap.Find(pInterceptor)->InterceptorTechnoType->InterceptorType.get(); + + if (!pTypeExt->Armor.isset()) + { + if (!pInterceptorType->KeepIntact) + this->InterceptedStatus |= InterceptedStatus::Intercepted; + } + else + { + const double versus = GeneralUtils::GetWarheadVersusArmor(pInterceptor->WH, pTypeExt->Armor.Get()); + + if (versus == 0.0) + return; + + const int damage = static_cast(pInterceptor->Health * versus); + this->CurrentStrength -= damage; + + if (Phobos::DisplayDamageNumbers && damage != 0) + GeneralUtils::DisplayDamageNumberString(damage, DamageDisplayType::Intercept, pThis->GetRenderCoords(), this->DamageNumberOffset); + + if (this->CurrentStrength <= 0) + { + this->CurrentStrength = 0; + + if (!pInterceptorType->KeepIntact) + this->InterceptedStatus |= InterceptedStatus::Intercepted; + } + } + + this->DetonateOnInterception = !pInterceptorType->DeleteOnIntercept.Get(pTypeExt->Interceptable_DeleteOnIntercept); + + if (const auto pWeaponOverride = pInterceptorType->WeaponOverride.Get(pTypeExt->Interceptable_WeaponOverride)) + { + pThis->WeaponType = pWeaponOverride; + pThis->Health = pInterceptorType->WeaponCumulativeDamage.Get() ? pThis->Health + pWeaponOverride->Damage : pWeaponOverride->Damage; + pThis->WH = pWeaponOverride->Warhead; + pThis->Bright = pWeaponOverride->Bright; + + if (pInterceptorType->WeaponReplaceProjectile + && pWeaponOverride->Projectile + && pWeaponOverride->Projectile != pThis->Type) + { + pThis->Speed = pWeaponOverride->Speed; + pThis->Type = pWeaponOverride->Projectile; + pTypeExt = BulletTypeExt::ExtMap.Find(pThis->Type); + this->TypeExtData = pTypeExt; + + if (this->LaserTrails.size()) + this->LaserTrails.clear(); + + if (!pThis->Type->Inviso) + this->InitializeLaserTrails(); + + // Lose target if the current bullet is no longer interceptable. + if (pSource && (!pTypeExt->Interceptable || (pTypeExt->Armor.isset() && GeneralUtils::GetWarheadVersusArmor(pInterceptor->WH, pTypeExt->Armor.Get()) == 0.0))) + pSource->SetTarget(nullptr); + } + } +} + +void BulletExt::ExtData::ApplyRadiationToCell(CellStruct cell, int spread, int radLevel) +{ + const auto pCell = MapClass::Instance.TryGetCellAt(cell); + + if (!pCell) + return; + + const auto pThis = this->OwnerObject(); + const auto pWeapon = pThis->GetWeaponType(); + const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon); + const auto pRadType = pWeaponExt->RadType; + const auto pCellExt = CellExt::ExtMap.Find(pCell); + + const auto it = std::find_if(pCellExt->RadSites.cbegin(), pCellExt->RadSites.cend(), + [=](const auto pSite) + { + const auto pRadExt = RadSiteExt::ExtMap.Find(pSite); + + if (pRadExt->Type != pRadType || spread != pSite->Spread) + return false; + + if (pRadExt->RadInvoker && pThis->Owner) + return pRadExt->RadInvoker == pThis->Owner; + + return true; + } + ); + + if (it != pCellExt->RadSites.cend()) + { + const auto pRadExt = RadSiteExt::ExtMap.Find(*it); + // Handle It + pRadExt->Add(std::min(radLevel, pRadType->GetLevelMax() - (*it)->GetRadLevel())); + return; + } + + const auto pThisHouse = pThis->Owner ? pThis->Owner->Owner : this->FirerHouse; + RadSiteExt::CreateInstance(cell, spread, radLevel, pWeaponExt, pThisHouse, pThis->Owner); +} + +void BulletExt::ExtData::InitializeLaserTrails() +{ + if (this->LaserTrails.size()) + return; + + auto const pThis = this->OwnerObject(); + auto const pTypeExt = BulletTypeExt::ExtMap.Find(pThis->Type); + auto const pOwner = pThis->Owner ? pThis->Owner->Owner : nullptr; + this->LaserTrails.reserve(pTypeExt->LaserTrail_Types.size()); + + for (auto const& idxTrail : pTypeExt->LaserTrail_Types) + this->LaserTrails.emplace_back(std::make_unique(LaserTrailTypeClass::Array[idxTrail].get(), pOwner)); +} + +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 (const auto pBuilding = abstract_cast(pAttach)) + pAnim->ZAdjust = SetBuildingFireAnimZAdjust(pBuilding, 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]; + 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) +{ + // Can not use 0x6FD210 because the firer may die + 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, BulletExt::GetTargetCoordsForFiring(pBullet), + ((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, BulletExt::GetTargetCoordsForFiring(pBullet), + 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) +{ + // Can not use 0x6FD460 because the firer may die + const auto pWeapon = pBullet->WeaponType; + + if (!pWeapon->IsElectricBolt) + return; + + const auto pBolt = EBoltExt::CreateEBolt(pWeapon); + pBolt->AlternateColor = pWeapon->IsAlternateColor; + + const auto targetCoords = BulletExt::GetTargetCoordsForFiring(pBullet); + const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon); + int zAdjust = pWeaponExt->EBoltZAdjust.Get(RulesExt::Global()->EBoltZAdjust); + + const auto pOwner = pBullet->Owner; + if (pOwner && pOwner->WhatAmI() == AbstractType::Building) + { + const bool clamp = pWeaponExt->EBoltZAdjust_ClampInitialDepthForBuilding.Get(RulesExt::Global()->EBoltZAdjust_ClampInitialDepthForBuilding); + if (clamp && zAdjust > 0) + zAdjust = 0; + } + + pBolt->Fire(pBullet->SourceCoords, targetCoords, zAdjust); + + if (const auto particle = WeaponTypeExt::ExtMap.Find(pWeapon)->Bolt_ParticleSystem.Get(RulesClass::Instance->DefaultSparkSystem)) + GameCreate(particle, targetCoords, nullptr, nullptr, CoordStruct::Empty, nullptr); +} + +// 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 auto pWH = pWeapon->Warhead; + const bool isTemporal = pWH && pWH->Temporal; + const auto pRadBeam = RadBeam::Allocate(isTemporal ? RadBeamType::Temporal : RadBeamType::RadBeam); + + pRadBeam->SetCoordsSource(pBullet->SourceCoords); + pRadBeam->SetCoordsTarget(BulletExt::GetTargetCoordsForFiring(pBullet)); + + 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, BulletExt::GetTargetCoordsForFiring(pBullet), pHouse); + } +} + +// Make sure pBullet is not empty before call +void BulletExt::SimulatedFiringUnlimbo(BulletClass* pBullet, HouseClass* pHouse, WeaponTypeClass* pWeapon, const CoordStruct& sourceCoords, bool headToTarget, const RadialFireStruct& radialFire) +{ + // Initialize bullet characteristics such as weapon type, range, house etc. + const auto pType = pBullet->Type; + const int projectileRange = WeaponTypeExt::ExtMap.Find(pWeapon)->ProjectileRange.Get(); + auto velocity = BulletVelocity::Empty; + pBullet->WeaponType = pWeapon; + pBullet->Range = projectileRange; + BulletExt::ExtMap.Find(pBullet)->FirerHouse = pHouse; + + if (pType->FirersPalette) + pBullet->InheritedColor = pHouse->ColorSchemeIndex; + + // If someone asks me, I would say Arcing is just a piece of shit + // But there are still people who like to use it, so anyway, it has been fixed + if (pType->Arcing) + { + // The target must exist during launch + const auto targetCoords = pBullet->Target->GetCenterCoords(); + const auto gravity = BulletTypeExt::GetAdjustedGravity(pType); + const auto distanceCoords = targetCoords - sourceCoords; + const auto horizontalDistance = Point2D { distanceCoords.X, distanceCoords.Y }.Magnitude(); + const bool lobber = pWeapon->Lobber || static_cast(horizontalDistance) < distanceCoords.Z; // 0x70D590 + // The lower the horizontal velocity, the higher the trajectory + // WW calculates the launch angle (and limits it) before calculating the velocity + // Here, some magic numbers are used to directly simulate its calculation + const auto speedMult = (lobber ? 0.45 : (distanceCoords.Z > 0 ? 0.68 : 1.0)); // Simulated 0x48A9D0 + const auto speed = speedMult * sqrt(horizontalDistance * gravity * 1.2); // 0x48AB90 + + // Simulate firing Arcing bullet + if (horizontalDistance < 1e-10 || speed < 1e-10) + { + // No solution + velocity.Z = speed; + } + else + { + const auto mult = speed / horizontalDistance; + velocity.X = static_cast(distanceCoords.X) * mult; + velocity.Y = static_cast(distanceCoords.Y) * mult; + velocity.Z = static_cast(distanceCoords.Z) * mult + (gravity * horizontalDistance) / (2 * speed); + } + } + else + { + const double speed = pBullet->Speed; + + if (headToTarget) // Home in on target. + { + const auto targetCoords = pBullet->Target->GetCenterCoords(); + const auto distanceCoords = targetCoords - sourceCoords; + + Vector3D distanceVector { + static_cast(distanceCoords.X), + static_cast(distanceCoords.Y), + static_cast(distanceCoords.Z) }; + + double len = distanceVector.Magnitude(); + + if (len > 0.0) + { + distanceVector /= len; + velocity = { distanceVector.X * speed, distanceVector.Y * speed, distanceVector.Z * speed }; + } + } + else // Drop down. + { + 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 * speed; + const auto radians = dir.GetRadian<32>(); + velocity = { Math::cos(radians) * flatSpeed, Math::sin(radians) * flatSpeed, -speed }; + } + } + + if (radialFire.Segments > 0) + velocity = ApplyRadialFireVelocityWarp(velocity, radialFire); + + // Unlimbo + pBullet->MoveTo(sourceCoords, velocity); +} + +BulletVelocity BulletExt::ApplyRadialFireVelocityWarp(BulletVelocity velocity, const RadialFireStruct& radialFire) +{ + if (radialFire.Segments <= 0) + return velocity; + + const double speedXY = std::hypot(velocity.X, velocity.Y); + + if (speedXY <= 0.0) + return velocity; + + const double offset = + (Math::Pi / radialFire.Segments) + * radialFire.Index + - Math::HalfPi; + + const double baseAngle = radialFire.Direction.GetRadian<32>(); + const double angle = baseAngle + offset; + + velocity.X = std::cos(angle) * speedXY; + velocity.Y = -std::sin(angle) * speedXY; + + return 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); + } +} + +CoordStruct BulletExt::GetTargetCoordsForFiring(BulletClass* pBullet) +{ + if (pBullet->Type->Inviso && pBullet->Type->FlakScatter) + return pBullet->Location; + else if (const auto pTarget = abstract_cast(pBullet->Target)) + return pTarget->GetTargetCoords(); + + return pBullet->TargetCoords; +} + +void BulletExt::ApplyArcingFix(BulletClass* pThis, const CoordStruct& sourceCoords, const CoordStruct& targetCoords, BulletVelocity& velocity) +{ + const auto distanceCoords = targetCoords - sourceCoords; + const auto horizontalDistance = Point2D { distanceCoords.X, distanceCoords.Y }.Magnitude(); + const bool lobber = pThis->WeaponType->Lobber || static_cast(horizontalDistance) < distanceCoords.Z; // 0x70D590 + // The lower the horizontal velocity, the higher the trajectory + // WW calculates the launch angle (and limits it) before calculating the velocity + // Here, some magic numbers are used to directly simulate its calculation + const auto speedMult = (lobber ? 0.45 : (distanceCoords.Z > 0 ? 0.68 : 1.0)); // Simulated 0x48A9D0 + const auto gravity = BulletTypeExt::GetAdjustedGravity(pThis->Type); + const auto speed = speedMult * sqrt(horizontalDistance * gravity * 1.2); // 0x48AB90 + + if (horizontalDistance < 1e-10 || speed < 1e-10) + { + // No solution + velocity.Z = speed; + } + else + { + const auto mult = speed / horizontalDistance; + velocity.X = static_cast(distanceCoords.X) * mult; + velocity.Y = static_cast(distanceCoords.Y) * mult; + velocity.Z = (static_cast(distanceCoords.Z) + velocity.Z) * mult + (gravity * horizontalDistance) / (2 * speed); + } +} + +// Detonate weapon/warhead using a bullet. +void BulletExt::Detonate(const CoordStruct& coords, TechnoClass* pOwner, int damage, HouseClass* pFiringHouse, AbstractClass* pTarget, bool isBright, WeaponTypeClass* pWeapon, WarheadTypeClass* pWarhead) +{ + auto const pType = pWeapon ? pWeapon->Projectile : BulletTypeExt::GetDefaultBulletType(); + auto const pBullet = pType->CreateBullet(pTarget, pOwner, damage, pWarhead, 100, isBright); + pBullet->WeaponType = pWeapon; + + auto const pBulletExt = BulletExt::ExtMap.Find(pBullet); + pBulletExt->IsInstantDetonation = true; + + if (pFiringHouse) + pBulletExt->FirerHouse = pFiringHouse; + + pBullet->SetLocation(coords); + pBullet->Explode(true); + pBullet->UnInit(); +} + +// ============================= +// load / save + +template +void BulletExt::ExtData::Serialize(T& Stm) +{ + Stm + .Process(this->TypeExtData) + .Process(this->FirerHouse) + .Process(this->CurrentStrength) + .Process(this->InterceptorTechnoType) + .Process(this->InterceptedStatus) + .Process(this->DetonateOnInterception) + .Process(this->LaserTrails) + .Process(this->SnappedToTarget) + .Process(this->DamageNumberOffset) + .Process(this->ParabombFallRate) + .Process(this->IsInstantDetonation) + .Process(this->FirepowerMult) + .Process(this->VectorType) + .Process(this->Vector.Initialized) + .Process(this->Vector.CurrentFrame) + .Process(this->Vector.DisabledTimer) + .Process(this->Vector.CurrentSpeed) + .Process(this->Vector.CurrentAngle) + .Process(this->Vector.CurrentCircleRadius) + .Process(this->Vector.CurrentCircleSpeed) + .Process(this->Vector.CurrentCircleAngle) + .Process(this->Vector.InitialOriginPos) + .Process(this->Vector.InitialLocation) + .Process(this->Vector.PrevCirclePos) + .Process(this->Vector.ArcHeight) + .Process(this->Vector.ArcRotation) + .Process(this->Vector.TargetOffset) + .Process(this->Vector.NormalRotF) + .Process(this->Vector.NormalRotL) + .Process(this->Vector.NormalRotH) + .Process(this->Vector.NormalStepF) + .Process(this->Vector.NormalStepL) + .Process(this->Vector.NormalStepH) + .Process(this->Vector.MovementFrames) + .Process(this->Vector.FacingRad) + .Process(this->Vector.TiltRad) + .Process(this->Vector.OriginOffset) + .Process(this->Vector.PrevCircleCenter) + .Process(this->Vector.OriginElapsed) + .Process(this->Vector.OriginSpeed) + .Process(this->Vector.OriginAngle) + .Process(this->Vector.OriginTargetOffset) + .Process(this->Vector.OriginCircleRadiusRuntime) + .Process(this->Vector.OriginCircleSpeedRuntime) + .Process(this->Vector.OriginCircleAngleRuntime) + .Process(this->Vector.OriginNormalRotFRuntime) + .Process(this->Vector.OriginNormalRotLRuntime) + .Process(this->Vector.OriginNormalRotHRuntime) + .Process(this->Vector.OriginNormalStepF) + .Process(this->Vector.OriginNormalStepL) + .Process(this->Vector.OriginNormalStepH) + .Process(this->Vector.OriginFacing) + .Process(this->Vector.OriginTilt) + .Process(this->Trajectory) + ; +} + +void BulletExt::ExtData::LoadFromStream(PhobosStreamReader& Stm) +{ + Extension::LoadFromStream(Stm); + this->Serialize(Stm); +} + +void BulletExt::ExtData::SaveToStream(PhobosStreamWriter& Stm) +{ + Extension::SaveToStream(Stm); + this->Serialize(Stm); +} + +// ============================= +// container + +BulletExt::ExtContainer::ExtContainer() : Container("BulletClass") { } + +BulletExt::ExtContainer::~ExtContainer() = default; + +#include +#include + +void BulletExt::ExtData::VectorAI() +{ + VectorAI_Run(this->OwnerObject(), this->VectorType, this->Vector, this->OwnerObject() ? static_cast(this->OwnerObject())->Owner : nullptr, true); +} + +// ============================= +// container hooks + +DEFINE_HOOK(0x4664BA, BulletClass_CTOR, 0x5) +{ + GET(BulletClass*, pItem, ESI); + + BulletExt::ExtMap.TryAllocate(pItem); + + return 0; +} + +DEFINE_HOOK(0x4665E9, BulletClass_DTOR, 0xA) +{ + GET(BulletClass*, pItem, ESI); + BulletExt::ExtMap.Remove(pItem); + return 0; +} + +DEFINE_HOOK_AGAIN(0x46AFB0, BulletClass_SaveLoad_Prefix, 0x8) +DEFINE_HOOK(0x46AE70, BulletClass_SaveLoad_Prefix, 0x5) +{ + GET_STACK(BulletClass*, pItem, 0x4); + GET_STACK(IStream*, pStm, 0x8); + + BulletExt::ExtMap.PrepareStream(pItem, pStm); + + return 0; +} + +DEFINE_HOOK_AGAIN(0x46AF97, BulletClass_Load_Suffix, 0x7) +DEFINE_HOOK(0x46AF9E, BulletClass_Load_Suffix, 0x7) +{ + BulletExt::ExtMap.LoadStatic(); + return 0; +} + +DEFINE_HOOK(0x46AFC4, BulletClass_Save_Suffix, 0x3) +{ + BulletExt::ExtMap.SaveStatic(); + return 0; +} diff --git a/VectorPort/src/Ext/Bullet/Body.h b/VectorPort/src/Ext/Bullet/Body.h new file mode 100644 index 0000000000..c2bbb17b63 --- /dev/null +++ b/VectorPort/src/Ext/Bullet/Body.h @@ -0,0 +1,104 @@ +#pragma once +#include + +#include +#include +#include +#include +#include + +struct RadialFireStruct +{ + int Segments = 0; + int Index = 0; + DirStruct Direction {}; +}; + +class BulletExt +{ +public: + using base_type = BulletClass; + + static constexpr DWORD Canary = 0x2A2A2A2A; + static constexpr size_t ExtPointerOffset = 0x18; + + class ExtData final : public Extension + { + public: + BulletTypeExt::ExtData* TypeExtData; + HouseClass* FirerHouse; + int CurrentStrength; + TechnoTypeExt::ExtData* InterceptorTechnoType; + InterceptedStatus InterceptedStatus; + bool DetonateOnInterception; + std::vector> LaserTrails; + bool SnappedToTarget; // Used for custom trajectory projectile target snap checks + int DamageNumberOffset; + int ParabombFallRate; + bool IsInstantDetonation; + double FirepowerMult; + + TrajectoryPointer Trajectory; + + AttachEffectTypeClass* VectorType; + VectorState Vector; + + void VectorAI(); + + ExtData(BulletClass* OwnerObject) : Extension(OwnerObject) + , TypeExtData { nullptr } + , FirerHouse { nullptr } + , CurrentStrength { 0 } + , InterceptorTechnoType { nullptr } + , InterceptedStatus { InterceptedStatus::None } + , DetonateOnInterception { true } + , LaserTrails {} + , Trajectory { nullptr } + , VectorType { nullptr } + , Vector {} + , SnappedToTarget { false } + , DamageNumberOffset { INT32_MIN } + , ParabombFallRate { 0 } + , IsInstantDetonation { false } + , FirepowerMult { 1.0 } + { } + + virtual ~ExtData() = default; + + virtual void InvalidatePointer(void* ptr, bool bRemoved) override { } + + virtual void LoadFromStream(PhobosStreamReader& Stm) override; + virtual void SaveToStream(PhobosStreamWriter& Stm) override; + + void InterceptBullet(TechnoClass* pSource, BulletClass* pInterceptor); + void ApplyRadiationToCell(CellStruct cell, int spread, int radLevel); + void InitializeLaserTrails(); + + private: + template + void Serialize(T& Stm); + }; + + class ExtContainer final : public Container + { + public: + ExtContainer(); + ~ExtContainer(); + }; + + static ExtContainer ExtMap; + + static void Detonate(const CoordStruct& coords, TechnoClass* pOwner, int damage, HouseClass* pFiringHouse, AbstractClass* pTarget, bool isBright, WeaponTypeClass* pWeapon, WarheadTypeClass* pWarhead); + static void ApplyArcingFix(BulletClass* pThis, const CoordStruct& sourceCoords, const CoordStruct& targetCoords, BulletVelocity& velocity); + static CoordStruct GetTargetCoordsForFiring(BulletClass* pBullet); + + static void SimulatedFiringUnlimbo(BulletClass* pBullet, HouseClass* pHouse, WeaponTypeClass* pWeapon, const CoordStruct& sourceCoords, bool headToTarget, const RadialFireStruct& radialFire = {}); + 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); + static inline BulletVelocity ApplyRadialFireVelocityWarp(BulletVelocity velocity, const RadialFireStruct& radialFire); +}; diff --git a/VectorPort/src/Ext/Bullet/Hooks.cpp b/VectorPort/src/Ext/Bullet/Hooks.cpp new file mode 100644 index 0000000000..9c3617846b --- /dev/null +++ b/VectorPort/src/Ext/Bullet/Hooks.cpp @@ -0,0 +1,627 @@ +#include "Body.h" +#include +#include +#include +#include + +// has everything inited except SpawnNextAnim at this point +DEFINE_HOOK(0x466556, BulletClass_Init, 0x6) +{ + GET(BulletClass*, pThis, ECX); + + if (auto const pExt = BulletExt::ExtMap.TryFind(pThis)) + { + if (pThis->Owner) + { + pExt->FirerHouse = pThis->Owner->Owner; + pExt->FirepowerMult = TechnoExt::GetCurrentFirepowerMultiplier(pThis->Owner); + + // Init Vector from firing techno's AE types + auto const pTechnoExt = TechnoExt::ExtMap.Find(pThis->Owner); + for (auto const& ae : pTechnoExt->AttachedEffects) + { + auto const pAEType = ae->GetType(); + if (!pAEType) continue; + if (pAEType->HasVector() && pAEType->Vector_AffectBullets) + { + pExt->VectorType = pAEType; + break; + } + } + } + + auto const pType = pThis->Type; + pExt->CurrentStrength = pType->Strength; + pExt->TypeExtData = BulletTypeExt::ExtMap.Find(pType); + + if (!pType->Inviso) + pExt->InitializeLaserTrails(); + } + + return 0; +} + +// Set in BulletClass::AI and guaranteed to be valid within it. +namespace BulletAITemp +{ + BulletExt::ExtData* ExtData; + BulletTypeExt::ExtData* TypeExtData; +} + +DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6) +{ + GET(BulletClass*, pThis, EBP); + + const auto pBulletExt = BulletExt::ExtMap.Find(pThis); + const auto pBulletTypeExt = pBulletExt->TypeExtData; + BulletAITemp::ExtData = pBulletExt; + BulletAITemp::TypeExtData = pBulletTypeExt; + + if (pBulletExt->VectorType) + { + if (pBulletExt->Vector.CurrentFrame >= pBulletExt->VectorType->Duration) + pBulletExt->VectorType = nullptr; + else + pBulletExt->VectorAI(); + } + + if (pBulletExt->InterceptedStatus & InterceptedStatus::Targeted) + { + if (const auto pTarget = abstract_cast(pThis->Target)) + { + const auto pTargetExt = BulletExt::ExtMap.Find(pTarget); + + if (!pTargetExt->TypeExtData->Armor.isset()) + pTargetExt->InterceptedStatus |= InterceptedStatus::Locked; + } + } + + if (pBulletExt->InterceptedStatus & InterceptedStatus::Intercepted) + { + if (const auto pTarget = abstract_cast(pThis->Target)) + BulletExt::ExtMap.Find(pTarget)->InterceptedStatus &= ~InterceptedStatus::Locked; + + if (pBulletExt->DetonateOnInterception) + pThis->Detonate(pThis->GetCoords()); + + pThis->Limbo(); + pThis->UnInit(); + + const auto pTechno = pThis->Owner; + const auto pWeapon = pThis->WeaponType; + + if (pTechno + && pTechno->InLimbo + && pWeapon + && pWeapon->LimboLaunch) + { + pThis->SetTarget(nullptr); + auto damage = pTechno->Health * 2; + pTechno->SetLocation(pThis->GetCoords()); + pTechno->ReceiveDamage(&damage, 0, RulesClass::Instance->C4Warhead, nullptr, true, false, nullptr); + } + } + + // VectorAI moved to BulletClass_AI_Trailer for correct engine-movement-final override + + if (!pBulletExt->Trajectory && pBulletExt->LaserTrails.size()) + { + const CoordStruct location = pThis->GetCoords(); + const BulletVelocity& velocity = pThis->Velocity; + + // We adjust LaserTrails to account for vanilla bug of drawing stuff one frame ahead. + // Pretty meh solution but works until we fix the bug - Kerbiter + CoordStruct drawnCoords + { + (int)(location.X + velocity.X), + (int)(location.Y + velocity.Y), + (int)(location.Z + velocity.Z) + }; + + for (const auto& pTrail : pBulletExt->LaserTrails) + { + // We insert initial position so the first frame of trail doesn't get skipped - Kerbiter + // TODO move hack to BulletClass creation + if (!pTrail->LastLocation.isset()) + pTrail->LastLocation = location; + + pTrail->Update(drawnCoords); + } + + } + + if (pThis->HasParachute) + { + int fallRate = pBulletExt->ParabombFallRate - pBulletTypeExt->Parachuted_FallRate; + const int maxFallRate = pBulletTypeExt->Parachuted_MaxFallRate.Get(RulesClass::Instance->ParachuteMaxFallRate); + + if (fallRate < maxFallRate) + fallRate = maxFallRate; + + pBulletExt->ParabombFallRate = fallRate; + pThis->FallRate = fallRate; + } + + return 0; +} + +DEFINE_HOOK(0x466897, BulletClass_AI_Trailer, 0x6) +{ + enum { SkipGameCode = 0x4668BD }; + + GET(BulletClass*, pThis, EBP); + REF_STACK(const CoordStruct, coords, STACK_OFFSET(0x1A8, -0x184)); + + auto const pTrailerAnim = GameCreate(pThis->Type->Trailer, coords, 1, 1); + auto const pTrailerAnimExt = AnimExt::ExtMap.Find(pTrailerAnim); + auto const pOwner = pThis->Owner ? pThis->Owner->Owner : BulletAITemp::ExtData->FirerHouse; + AnimExt::SetAnimOwnerHouseKind(pTrailerAnim, pOwner, nullptr, false, true); + pTrailerAnimExt->SetInvoker(pThis->Owner); + + return SkipGameCode; +} + +// Inviso bullets behave differently in BulletClass::AI when their target is bullet and +// seemingly (at least partially) adopt characteristics of a vertical projectile. +// This is a potentially slightly hacky solution to that, as proper solution +// would likely require making sense of BulletClass::AI and ain't nobody got time for that. +DEFINE_HOOK(0x4668BD, BulletClass_AI_Interceptor_InvisoSkip, 0x6) +{ + enum { DetonateBullet = 0x467F9B }; + + GET(BulletClass*, pThis, EBP); + + if (auto const pExt = BulletAITemp::ExtData) + { + if (pThis->Type->Inviso && pExt->InterceptorTechnoType) + return DetonateBullet; + } + + return 0; +} + +#pragma region Gravity + +#define APPLYGRAVITY(pType)\ +auto const nGravity = BulletTypeExt::GetAdjustedGravity(pType);\ +__asm { fld nGravity };\ + +DEFINE_HOOK(0x4671B9, BulletClass_AI_ApplyGravity, 0x6) +{ + GET(BulletTypeClass* const, pType, EAX); + + APPLYGRAVITY(pType); + + return 0x4671BF; +} + +DEFINE_HOOK(0x6F7481, TechnoClass_Targeting_ApplyGravity, 0x6) +{ + GET(WeaponTypeClass* const, pWeaponType, EDX); + + APPLYGRAVITY(pWeaponType->Projectile); + + return 0x6F74A4; +} + +DEFINE_HOOK(0x6FDAA6, TechnoClass_FireAngle_6FDA00_ApplyGravity, 0x5) +{ + GET(WeaponTypeClass* const, pWeaponType, EDI); + + APPLYGRAVITY(pWeaponType->Projectile); + + return 0x6FDACE; +} + +DEFINE_HOOK(0x6FECB2, TechnoClass_FireAt_ApplyGravity, 0x6) +{ + GET(BulletTypeClass* const, pType, EAX); + + APPLYGRAVITY(pType); + + return 0x6FECD1; +} + +DEFINE_HOOK_AGAIN(0x44D2AE, BuildingClass_Mission_Missile_ApplyGravity, 0x6) +DEFINE_HOOK_AGAIN(0x44D264, BuildingClass_Mission_Missile_ApplyGravity, 0x6) +DEFINE_HOOK(0x44D074, BuildingClass_Mission_Missile_ApplyGravity, 0x6) +{ + GET(WeaponTypeClass* const, pWeaponType, EBP); + + APPLYGRAVITY(pWeaponType->Projectile); + + switch (R->Origin()) + { + case 0x44D074: + return 0x44D07A; + break; + case 0x44D264: + return 0x44D26A; + break; + case 0x44D2AE: + return 0x44D2B4; + break; + default: + __assume(0); + } +} + +#pragma endregion + +namespace ShrapnelTemp +{ + BuildingClass* InitialTargetBuilding = nullptr; + std::unordered_set TargetsToIgnore; +} + +DEFINE_HOOK(0x46A3D6, BulletClass_Shrapnel_Forced, 0xA) +{ + enum { Shrapnel = 0x46A40C, Skip = 0x46ADCD }; + + GET(BulletClass*, pThis, EDI); + + auto const pTypeExt = BulletTypeExt::ExtMap.Find(pThis->Type); + ShrapnelTemp::InitialTargetBuilding = nullptr; + ShrapnelTemp::TargetsToIgnore.clear(); + + if (auto const pObject = pThis->GetCell()->FirstObject) + { + auto const rtti = pObject->WhatAmI(); + + if (rtti != AbstractType::Building) + { + return Shrapnel; + } + else if (pTypeExt->Shrapnel_AffectsBuildings) + { + ShrapnelTemp::InitialTargetBuilding = static_cast(pObject); + return Shrapnel; + } + } + else if (pTypeExt->Shrapnel_AffectsGround) + { + return Shrapnel; + } + + return Skip; +} + +DEFINE_HOOK(0x46A4FB, BulletClass_Shrapnel_Targeting, 0x6) +{ + enum { SkipObject = 0x46A8EA, Continue = 0x46A50F }; + + GET(BulletClass*, pThis, EDI); + GET(ObjectClass*, pObject, EBP); + GET(TechnoClass*, pSource, EAX); + GET(WeaponTypeClass*, pShrapnelWeapon, ESI); + + auto const pTypeExt = BulletTypeExt::ExtMap.Find(pThis->Type); + bool isBuilding = pObject->WhatAmI() == AbstractType::Building; + bool ignorePreviouslyHit = pTypeExt->Shrapnel_IgnoreHitBuildings.Get(RulesExt::Global()->Shrapnel_IgnoreHitBuildings); + + if (isBuilding) + { + // Do not fire shrapnels on the building itself if bouncing off one. + if (pObject == ShrapnelTemp::InitialTargetBuilding) + return SkipObject; + + if (ignorePreviouslyHit && ShrapnelTemp::TargetsToIgnore.contains(pObject)) + return SkipObject; + } + + auto const pOwner = pSource->Owner; + + if (pTypeExt->Shrapnel_UseWeaponTargeting) + { + auto const pWeaponExt = WeaponTypeExt::ExtMap.Find(pShrapnelWeapon); + auto const pType = pObject->GetType(); + + if (!pType->LegalTarget) + return SkipObject; + + if (!pWeaponExt->SkipWeaponPicking && !EnumFunctions::IsCellEligible(pObject->GetCell(), pWeaponExt->CanTarget, true, true)) + return SkipObject; + + auto const pWH = pShrapnelWeapon->Warhead; + auto armorType = pType->Armor; + + if (auto const pTechno = abstract_cast(pObject)) + { + if (!pWeaponExt->SkipWeaponPicking) + { + if (!EnumFunctions::CanTargetHouse(pWeaponExt->CanTargetHouses, pOwner, pTechno->Owner) || !EnumFunctions::IsTechnoEligible(pTechno, pWeaponExt->CanTarget) + || !pWeaponExt->IsHealthInThreshold(pTechno) || !pWeaponExt->IsVeterancyInThreshold(pTechno) || !pWeaponExt->HasRequiredAttachedEffects(pTechno, pSource)) + { + return SkipObject; + } + } + + auto const pShield = TechnoExt::ExtMap.Find(pTechno)->Shield.get(); + + if (pShield && pShield->IsActive() && !pShield->CanBePenetrated(pWH)) + armorType = pShield->GetArmorType(); + } + + if (GeneralUtils::GetWarheadVersusArmor(pWH, armorType) == 0.0) + return SkipObject; + } + else if (pOwner->IsAlliedWith(pObject)) + { + return SkipObject; + } + + if (isBuilding && ignorePreviouslyHit) + ShrapnelTemp::TargetsToIgnore.insert(pObject); + + return Continue; +} + +DEFINE_HOOK(0x46902C, BulletClass_Explode_Cluster, 0x6) +{ + enum { SkipGameCode = 0x469091 }; + + GET(BulletClass*, pThis, ESI); + REF_STACK(const CoordStruct, origCoords, STACK_OFFSET(0x3C, -0x30)); + + auto const pTypeExt = BulletTypeExt::ExtMap.Find(pThis->Type); + const int min = pTypeExt->ClusterScatter_Min.Get(); + const int max = pTypeExt->ClusterScatter_Max.Get(); + auto coords = origCoords; + auto& random = ScenarioClass::Instance->Random; + + for (int i = 0; i < pThis->Type->Cluster; i++) + { + pThis->Detonate(coords); + + if (!pThis->IsAlive) + break; + + const int distance = random.RandomRanged(min, max); + coords = MapClass::GetRandomCoordsNear(origCoords, distance, false); + } + + return SkipGameCode; +} + +constexpr bool CheckTrajectoryCanNotAlwaysSnap(const TrajectoryFlag flag) +{ + return flag != TrajectoryFlag::Invalid; +/* return flag == TrajectoryFlag::Straight + || flag == TrajectoryFlag::Bombard + || flag == TrajectoryFlag::Parabola;*/ +} + +DEFINE_HOOK(0x467CCA, BulletClass_AI_TargetSnapChecks, 0x6) +{ + enum { SkipChecks = 0x467CDE }; + + GET(BulletClass*, pThis, EBP); + + auto const pType = pThis->Type; + + // Do not require Airburst=no to check target snapping for Inviso / Trajectory=Straight projectiles + if (pType->Inviso) + { + R->EAX(pType); + return SkipChecks; + } + else if (auto const pExt = BulletAITemp::ExtData) + { + if (pExt->Trajectory && CheckTrajectoryCanNotAlwaysSnap(pExt->Trajectory->Flag())) + { + R->EAX(pType); + return SkipChecks; + } + } + + return 0; +} + +DEFINE_HOOK(0x468E61, BulletClass_Explode_TargetSnapChecks1, 0x6) +{ + enum { Snap = 0x468E7B, SkipChecks = 0x468FF4 }; + + GET(BulletClass*, pThis, ESI); + + auto const pExt = BulletExt::ExtMap.Find(pThis); + + if (pExt->IsInstantDetonation) + return SkipChecks; + + auto const pType = pThis->Type; + + // Do not require Airburst=no to check target snapping for Inviso / Trajectory=Straight projectiles + if (pType->Inviso) + { + R->EAX(pType); + return Snap; + } + else if (pType->Arcing || pType->ROT > 0) + { + return 0; + } + + if (pExt->Trajectory && CheckTrajectoryCanNotAlwaysSnap(pExt->Trajectory->Flag()) && !pExt->SnappedToTarget) + { + R->EAX(pType); + return Snap; + } + + return 0; +} + +DEFINE_HOOK(0x468E9F, BulletClass_Explode_TargetSnapChecks2, 0x6) +{ + enum { SkipInitialChecksOnly = 0x468EC7, SkipSetCoordinate = 0x468F23, SkipChecks = 0x468FF4 }; + + GET(BulletClass*, pThis, ESI); + + auto const pExt = BulletExt::ExtMap.Find(pThis); + + if (pExt->IsInstantDetonation) + return SkipChecks; + + auto const pType = pThis->Type; + + // Do not require EMEffect=no & Airburst=no to check target coordinate snapping for Inviso projectiles. + if (pType->Inviso) + { + R->EAX(pType); + return SkipInitialChecksOnly; + } + else if (pType->Arcing || pType->ROT > 0) + { + return 0; + } + + // Do not force Trajectory=Straight projectiles to detonate at target coordinates under certain circumstances. + // Fixes issues with walls etc. + if (pExt->Trajectory && CheckTrajectoryCanNotAlwaysSnap(pExt->Trajectory->Flag()) && !pExt->SnappedToTarget) + return SkipSetCoordinate; + + return 0; +} + +DEFINE_HOOK(0x468D3F, BulletClass_ShouldExplode_AirTarget, 0x6) +{ + enum { SkipCheck = 0x468D73 }; + + GET(BulletClass*, pThis, ESI); + + auto const pExt = BulletExt::ExtMap.Find(pThis); + + if (pExt->Trajectory && CheckTrajectoryCanNotAlwaysSnap(pExt->Trajectory->Flag())) + return SkipCheck; + + return 0; +} + +DEFINE_HOOK(0x4687F8, BulletClass_Unlimbo_FlakScatter, 0x6) +{ + GET(BulletClass*, pThis, EBX); + GET_STACK(const float, mult, STACK_OFFSET(0x5C, -0x44)); + + if (pThis->WeaponType) + { + auto const pTypeExt = BulletTypeExt::ExtMap.Find(pThis->Type); + const int defaultValue = RulesClass::Instance->BallisticScatter; + const int min = pTypeExt->BallisticScatter_Min.Get(Leptons(0)); + const int max = pTypeExt->BallisticScatter_Max.Get(Leptons(defaultValue)); + + const int result = (int)((mult * ScenarioClass::Instance->Random.RandomRanged(2 * min, 2 * max)) / pThis->WeaponType->Range); + R->EAX(result); + } + + return 0; +} + +// Skip a forced detonation check for Level=true projectiles that is now handled in Hooks.Obstacles.cpp. +DEFINE_JUMP(LJMP, 0x468D08, 0x468D2F); + +DEFINE_HOOK(0x6FF008, TechnoClass_Fire_BeforeMoveTo, 0x8) +{ + GET(BulletClass* const, pBullet, EBX); + + const auto pBulletType = pBullet->Type; + + if (pBulletType->Arcing && !BulletTypeExt::ExtMap.Find(pBulletType)->Arcing_AllowElevationInaccuracy) + { + REF_STACK(BulletVelocity, velocity, STACK_OFFSET(0xB0, -0x60)); + REF_STACK(const CoordStruct, crdSrc, STACK_OFFSET(0xB0, -0x6C)); + REF_STACK(const CoordStruct, crdOffset, STACK_OFFSET(0xB0, -0x1C)); + REF_STACK(const CoordStruct, fireCoords, STACK_OFFSET(0xB0, -0x6C)); + + const auto crdTgt = crdOffset + fireCoords; + BulletExt::ApplyArcingFix(pBullet, crdSrc, crdTgt, velocity); + } + + return 0; +} + +DEFINE_HOOK(0x44D46E, BuildingClass_Mission_Missile_BeforeMoveTo, 0x8) +{ + GET(BulletClass* const, pBullet, EDI); + + const auto pBulletType = pBullet->Type; + + if (pBulletType->Arcing && !BulletTypeExt::ExtMap.Find(pBulletType)->Arcing_AllowElevationInaccuracy) + { + REF_STACK(BulletVelocity, velocity, STACK_OFFSET(0xE8, -0xD0)); + REF_STACK(const CoordStruct, crdSrc, STACK_OFFSET(0xE8, -0x8C)); + REF_STACK(const CoordStruct, crdTgt, STACK_OFFSET(0xE8, -0x4C)); + + BulletExt::ApplyArcingFix(pBullet, crdSrc, crdTgt, velocity); + } + + return 0; +} + +// Vanilla inertia effect only for bullets with ROT=0 +DEFINE_HOOK(0x415F25, AircraftClass_Fire_TrajectorySkipInertiaEffect, 0x6) +{ + enum { SkipCheck = 0x4160BC }; + + GET(BulletClass*, pThis, ESI); + + if (BulletExt::ExtMap.Find(pThis)->Trajectory) + return SkipCheck; + + return 0; +} + +#pragma region Parabombs + +// Patch out Ares parabomb implementation. +DEFINE_PATCH(0x46867F, 0x6A, 0x00, 0x8B, 0xD9, 0x50); + +// Add in our own. +static bool __fastcall ObjectClass_Unlimbo_Parachuted_Wrapper(BulletClass* pThis, void*, const CoordStruct& coords, DirType facing) +{ + auto const pTypeExt = BulletTypeExt::ExtMap.Find(pThis->Type); + + if (pTypeExt->Parachuted) + return pThis->SpawnParachuted(coords); + + return pThis->Unlimbo(coords, facing); +} + +DEFINE_FUNCTION_JUMP(CALL, 0x468684, ObjectClass_Unlimbo_Parachuted_Wrapper); + +// Set parachute animation on projectile. +DEFINE_HOOK(0x5F5A62, ObjectClass_SpawnParachuted_BombParachute, 0x5) +{ + enum { SkipGameCode = 0x5F5A99 }; + + GET(BulletClass*, pThis, ESI); + GET(CoordStruct*, coords, EDI); + + auto const pTypeExt = BulletTypeExt::ExtMap.Find(pThis->Type); + auto const pAnimType = pTypeExt->BombParachute.Get(RulesClass::Instance->BombParachute); + AnimClass* pAnim = nullptr; + + if (pAnimType) + { + pAnim = GameCreate(pAnimType, *coords); + pAnim->Owner = pThis->Owner ? pThis->Owner->Owner : BulletExt::ExtMap.Find(pThis)->FirerHouse; + const int schemeIndex = pAnim->Owner ? pAnim->Owner->ColorSchemeIndex : RulesExt::Global()->AnimRemapDefaultColorScheme; + pAnim->LightConvert = ColorScheme::Array[schemeIndex]->LightConvert; + pThis->Parachute = pAnim; + } + + R->EAX(pAnim); + return SkipGameCode; +} + +DEFINE_HOOK(0x467AB2, BulletClass_AI_Parabomb, 0x7) +{ + enum { SkipGameCode = 0x467B1A }; + + GET(BulletClass*, pThis, EBP); + + if (pThis->HasParachute) + return SkipGameCode; + + return 0; +} + +#pragma endregion diff --git a/VectorPort/src/Ext/Techno/Body.Update.cpp b/VectorPort/src/Ext/Techno/Body.Update.cpp new file mode 100644 index 0000000000..09d09e720a --- /dev/null +++ b/VectorPort/src/Ext/Techno/Body.Update.cpp @@ -0,0 +1,2278 @@ +// methods used in TechnoClass_AI hooks or anything similar +#include "Body.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + + +// TechnoClass_AI_0x6F9E50 +// It's not recommended to do anything more here it could have a better place for performance consideration +void TechnoExt::ExtData::OnEarlyUpdate() +{ + auto const pType = this->OwnerObject()->GetTechnoType(); + + // Set only if unset or type is changed + // Notice that Ares may handle type conversion in the same hook here, which is executed right before this one thankfully + if (!this->TypeExtData || this->TypeExtData->OwnerObject() != pType) + this->UpdateTypeData(pType); + + if (this->CheckDeathConditions()) + return; + + this->UpdateShield(); + this->UpdateAttachEffects(); + this->ApplyInterceptor(); + this->EatPassengers(); + this->ApplySpawnLimitRange(); + this->ApplyMindControlRangeLimit(); + this->UpdateRecountBurst(); + this->UpdateRearmInEMPState(); + this->UpdateLastTargetCrd(); + + if (this->AttackMoveFollowerTempCount) + this->AttackMoveFollowerTempCount--; +} + +void TechnoExt::ExtData::ApplyInterceptor() +{ + const auto pTypeExt = this->TypeExtData; + const auto pInterceptorType = pTypeExt->InterceptorType.get(); + + if (!pInterceptorType || Unsorted::CurrentFrame % pInterceptorType->TargetingDelay != 0) + return; + + const auto pThis = this->OwnerObject(); + + if (!BulletClass::Array.Count || this->IsBurrowed || !pThis->IsArmed()) + return; + + const auto pTarget = pThis->Target; + + if (pTarget) + { + if (pTarget->WhatAmI() != AbstractType::Bullet) + return; + + const auto pTargetExt = BulletExt::ExtMap.Find(static_cast(pTarget)); + + if ((pTargetExt->InterceptedStatus & InterceptedStatus::Locked) == InterceptedStatus::None) + return; + } + + const bool isBuilding = pThis->WhatAmI() == AbstractType::Building; + + if (isBuilding && (pThis->CurrentMission == Mission::Selling || pThis->CurrentMission == Mission::Construction)) + return; + + BulletClass* pOptionalTarget = nullptr; + BulletClass* pTargetBullet = nullptr; + const double guardRange = pInterceptorType->GuardRange.Get(pThis); + const double guardRangeSq = guardRange * guardRange; + const double minGuardRange = pInterceptorType->MinimumGuardRange.Get(pThis); + const double minGuardRangeSq = minGuardRange * minGuardRange; + const auto location = pThis->Location; + const auto pWeapon = pThis->GetWeapon(pInterceptorType->Weapon)->WeaponType; // Interceptor weapon is always fixed + const auto pWH = pWeapon->Warhead; + const auto pOwner = pThis->Owner; + + for (auto const pBullet : BulletClass::Array) + { + const auto pBulletExt = BulletExt::ExtMap.Find(pBullet); + const auto pBulletTypeExt = pBulletExt->TypeExtData; + + if (!pBulletTypeExt->Interceptable || pBullet->SpawnNextAnim) + continue; + + const bool isTargetedOrLocked = static_cast(pBulletExt->InterceptedStatus & (InterceptedStatus::Targeted | InterceptedStatus::Locked)); + + // If we already have an optional target skip ones that are already being targeted etc. + if (pOptionalTarget && isTargetedOrLocked) + continue; + + auto bulletLoc = pBullet->Location; + + if (pInterceptorType->GuardRange_IsCylindrical) + bulletLoc.Z = location.Z; + + const auto distanceSq = bulletLoc.DistanceFromSquared(location); + + if (distanceSq > guardRangeSq || distanceSq < minGuardRangeSq) + continue; + + if (pBulletTypeExt->Armor.isset()) + { + if (!GeneralUtils::GetWarheadVersusArmor(pWH, pBulletTypeExt->Armor.Get())) + continue; + } + + const auto bulletOwner = pBullet->Owner ? pBullet->Owner->Owner : pBulletExt->FirerHouse; + + if (!EnumFunctions::CanTargetHouse(pInterceptorType->CanTargetHouses, pOwner, bulletOwner)) + continue; + + if (!pOptionalTarget && isTargetedOrLocked) + { + pOptionalTarget = pBullet; // Set as optional target + continue; + } + + // Establish target + pTargetBullet = pBullet; + break; + } + + // There is no more suitable target, establish optional target + if (!pTargetBullet && pOptionalTarget) + pTargetBullet = pOptionalTarget; + + if (pTargetBullet) + { + pThis->SetTarget(pTargetBullet); + + // Skip normal transition from idle to attack for building interceptors. + if (isBuilding) + { + pThis->QueueMission(Mission::Attack, false); + pThis->NextMission(); + } + } +} + +void TechnoExt::ExtData::DepletedAmmoActions() +{ + auto const pTypeExt = this->TypeExtData; + const int min = pTypeExt->Ammo_AutoDeployMinimumAmount; + const int max = pTypeExt->Ammo_AutoDeployMaximumAmount; + + if (min < 0 && max < 0) + return; + + auto const pType = pTypeExt->OwnerObject(); + + if (pType->Ammo <= 0) + return; + + auto const pThis = static_cast(this->OwnerObject()); + auto const pUnitType = pThis->Type; + + if (!pUnitType->IsSimpleDeployer && !pUnitType->DeploysInto && !pUnitType->DeployFire + && pUnitType->Passengers < 1 && pThis->Passengers.NumPassengers < 1) + { + return; + } + + const int ammo = pThis->Ammo; + const bool canDeploy = TechnoExt::HasAmmoToDeploy(pThis) && (min < 0 || ammo >= min) && (max < 0 || ammo <= max); + const bool isDeploying = pThis->CurrentMission == Mission::Unload || pThis->QueuedMission == Mission::Unload; + + if (canDeploy && !isDeploying) + { + pThis->QueueMission(Mission::Unload, true); + } + else if (!canDeploy && isDeploying) + { + pThis->QueueMission(Mission::Guard, true); + + if (pUnitType->IsSimpleDeployer && pThis->InAir) + { + if (auto const pJJLoco = locomotion_cast(pThis->Locomotor)) + pJJLoco->State = JumpjetLocomotionClass::State::Ascending; + } + } +} + +void TechnoExt::ExtData::AmmoAutoConvertActions() +{ + const auto pTypeExt = this->TypeExtData; + + if (!pTypeExt->Ammo_AutoConvertType.isset()) + return; + + const int min = pTypeExt->Ammo_AutoConvertMinimumAmount; + const int max = pTypeExt->Ammo_AutoConvertMaximumAmount; + + if (min < 0 && max < 0) + return; + + if (pTypeExt->OwnerObject()->Ammo <= 0) + return; + + const auto pThis = this->OwnerObject(); + const int ammo = pThis->Ammo; + + if ((min < 0 || ammo >= min) && (max < 0 || ammo <= max)) + { + const auto pFoot = abstract_cast(pThis); + TechnoExt::ConvertToType(pFoot, pTypeExt->Ammo_AutoConvertType); + } +} + +// Subterranean harvester factory exit state machine. +void TechnoExt::ExtData::UpdateSubterraneanHarvester() +{ + auto const pThis = static_cast(this->OwnerObject()); + + // Unnecessary for AI players. + if (!pThis->Owner->IsControlledByHuman()) + return; + + switch (this->SubterraneanHarvStatus) + { + case 0: // No state to handle. + break; + case 1: // Unit has been created. + // If we're still in the factory do not advance. + if (pThis->HasAnyLink()) + break; + + pThis->ClearNavigationList(); + + // If we have rally point available, move to it and advance to next state, otherwise end here. + if (this->SubterraneanHarvRallyPoint) + { + pThis->SetDestination(this->SubterraneanHarvRallyPoint, false); + pThis->QueueMission(Mission::Move, true); + this->SubterraneanHarvRallyPoint = nullptr; + this->SubterraneanHarvStatus = 2; + break; + } + else + { + this->SubterraneanHarvStatus = 0; + } + + break; + case 2: // Out of factory and on move. + // If we're still moving don't start harvesting. + if (pThis->Destination || pThis->CurrentMission == Mission::Move) + break; + + // If harvester stops moving and becomes anything except idle, reset the state machine. + if (pThis->CurrentMission != Mission::Guard) + { + this->SubterraneanHarvStatus = 0; + break; + } + + // Go harvest ore. + pThis->ClearNavigationList(); + pThis->QueueMission(Mission::Harvest, true); + this->SubterraneanHarvStatus = 0; + break; + default: + break; + } +} + +// TODO : Merge into new AttachEffects +bool TechnoExt::ExtData::CheckDeathConditions(bool isInLimbo) +{ + auto const pTypeExt = this->TypeExtData; + + if (!pTypeExt->AutoDeath_Behavior.isset()) + return false; + + auto const pThis = this->OwnerObject(); + + // Self-destruction must be enabled + const auto howToDie = pTypeExt->AutoDeath_Behavior.Get(); + + // Death if no ammo + if (pTypeExt->OwnerObject()->Ammo > 0 && pThis->Ammo <= 0 && pTypeExt->AutoDeath_OnAmmoDepletion) + { + TechnoExt::KillSelf(pThis, howToDie, pTypeExt->AutoDeath_VanishAnimation, isInLimbo); + return true; + } + + // Death if countdown ends + if (pTypeExt->AutoDeath_AfterDelay > 0) + { + if (!this->AutoDeathTimer.HasStarted()) + { + this->AutoDeathTimer.Start(pTypeExt->AutoDeath_AfterDelay); + } + else if (this->AutoDeathTimer.Completed()) + { + TechnoExt::KillSelf(pThis, howToDie, pTypeExt->AutoDeath_VanishAnimation, isInLimbo); + return true; + } + } + + auto const pOwner = pThis->Owner; + + auto existTechnoTypes = [pOwner](const ValueableVector& vTypes, AffectedHouse affectedHouse, bool any, bool allowLimbo) + { + auto existSingleType = [pOwner, affectedHouse, allowLimbo](TechnoTypeClass* pType) + { + if (affectedHouse == AffectedHouse::Owner) + return allowLimbo ? HouseExt::ExtMap.Find(pOwner)->CountOwnedPresentAndLimboed(pType) > 0 : pOwner->CountOwnedAndPresent(pType) > 0; + + for (auto const pHouse : HouseClass::Array) + { + if (EnumFunctions::CanTargetHouse(affectedHouse, pOwner, pHouse) + && (allowLimbo ? HouseExt::ExtMap.Find(pHouse)->CountOwnedPresentAndLimboed(pType) > 0 : pHouse->CountOwnedAndPresent(pType) > 0)) + return true; + } + + return false; + }; + + return any + ? std::any_of(vTypes.begin(), vTypes.end(), existSingleType) + : std::all_of(vTypes.begin(), vTypes.end(), existSingleType); + }; + + // death if listed technos don't exist + if (!pTypeExt->AutoDeath_TechnosDontExist.empty()) + { + if (!existTechnoTypes(pTypeExt->AutoDeath_TechnosDontExist, pTypeExt->AutoDeath_TechnosDontExist_Houses, !pTypeExt->AutoDeath_TechnosDontExist_Any, pTypeExt->AutoDeath_TechnosDontExist_AllowLimboed)) + { + TechnoExt::KillSelf(pThis, howToDie, pTypeExt->AutoDeath_VanishAnimation, isInLimbo); + + return true; + } + } + + // death if listed technos exist + if (!pTypeExt->AutoDeath_TechnosExist.empty()) + { + if (existTechnoTypes(pTypeExt->AutoDeath_TechnosExist, pTypeExt->AutoDeath_TechnosExist_Houses, pTypeExt->AutoDeath_TechnosExist_Any, pTypeExt->AutoDeath_TechnosExist_AllowLimboed)) + { + TechnoExt::KillSelf(pThis, howToDie, pTypeExt->AutoDeath_VanishAnimation, isInLimbo); + + return true; + } + } + + return false; +} + +void TechnoExt::ExtData::EatPassengers() +{ + auto const pTypeExt = this->TypeExtData; + + if (!pTypeExt->PassengerDeletionType) + return; + + auto const pThis = this->OwnerObject(); + + if (!TechnoExt::IsActiveIgnoreEMP(pThis)) + return; + + auto const pDelType = pTypeExt->PassengerDeletionType.get(); + + if (!pDelType->UnderEMP && (pThis->Deactivated || pThis->IsUnderEMP())) + { + if (this->PassengerDeletionTimer.InProgress()) + this->PassengerDeletionTimer.StartTime++; + + return; + } + + if (pDelType->Rate > 0 || pDelType->UseCostAsRate) + { + if (pThis->Passengers.NumPassengers > 0) + { + // Passengers / CargoClass is essentially a stack, last in, first out (LIFO) kind of data structure + FootClass* pPoorGuy = nullptr; // Passenger to potentially delete + FootClass* pPreviousPassenger = nullptr; // Passenger immediately prior to the deleted one in the stack + ObjectClass* pLastPassenger = nullptr; // Passenger that is last in the stack + auto pCurrentPassenger = pThis->Passengers.GetFirstPassenger(); + const auto allowedHouses = pDelType->AllowedHouses; + const auto pOwner = pThis->Owner; + const bool displayCash = pDelType->DisplaySoylent && pThis->IsClearlyVisibleTo(HouseClass::CurrentPlayer); + + // Find the first entered passenger that is eligible for deletion. + while (pCurrentPassenger) + { + if (EnumFunctions::CanTargetHouse(allowedHouses, pOwner, pCurrentPassenger->Owner)) + { + pPreviousPassenger = abstract_cast(pLastPassenger); + pPoorGuy = pCurrentPassenger; + } + + pLastPassenger = pCurrentPassenger; + pCurrentPassenger = abstract_cast(pCurrentPassenger->NextObject); + } + + if (!pPoorGuy) + { + this->PassengerDeletionTimer.Stop(); + return; + } + + if (!this->PassengerDeletionTimer.IsTicking()) // Execute only if timer has been stopped or not started + { + int timerLength = 0; + + if (pDelType->UseCostAsRate) + { + // Use passenger cost as countdown. + timerLength = (int)(pPoorGuy->GetTechnoType()->Cost * pDelType->CostMultiplier); + + if (pDelType->CostRateCap.isset()) + timerLength = std::min(timerLength, pDelType->CostRateCap.Get()); + } + else + { + // Use explicit rate optionally multiplied by unit size as countdown. + timerLength = pDelType->Rate; + const double size = (double)pPoorGuy->GetTechnoType()->Size; + + if (pDelType->Rate_SizeMultiply && size > 1.0) + timerLength *= (int)(size + 0.5); + } + + this->PassengerDeletionTimer.Start(timerLength); + } + else if (this->PassengerDeletionTimer.Completed()) // Execute only if timer has ran out after being started + { + --pThis->Passengers.NumPassengers; + + if (pLastPassenger) + pLastPassenger->NextObject = nullptr; + + if (pPreviousPassenger) + pPreviousPassenger->NextObject = pPoorGuy->NextObject; + + if (pThis->Passengers.NumPassengers <= 0) + pThis->Passengers.FirstPassenger = nullptr; + + if (pDelType->ReportSound >= 0) + VocClass::PlayAt(pDelType->ReportSound.Get(), pThis->GetCoords(), nullptr); + + AnimExt::CreateRandomAnim(pDelType->Anim, pThis->Location, pThis, nullptr, true, true); + + // Check if there is money refund + if (pDelType->Soylent + && EnumFunctions::CanTargetHouse(pDelType->SoylentAllowedHouses, pOwner, pPoorGuy->Owner)) + { + const double multiplier = pDelType->SoylentMultiplier; + int moneyToGive = static_cast(pPoorGuy->GetTechnoType()->GetRefund(pPoorGuy->Owner, true) * multiplier); + + for (auto pPassenger = pPoorGuy->Passengers.GetFirstPassenger(); pPassenger; pPassenger = abstract_cast(pPassenger->NextObject)) + moneyToGive += static_cast(pPassenger->GetTechnoType()->GetRefund(pPassenger->Owner, true) * multiplier); + + if (const auto pParasite = pPoorGuy->ParasiteEatingMe) + { + moneyToGive += static_cast(pParasite->GetTechnoType()->GetRefund(pParasite->Owner, true) * multiplier); + pParasite->ParasiteImUsing->SuppressionTimer.Start(50); + pParasite->ParasiteImUsing->ExitUnit(); + } + + const int hijack = pPoorGuy->HijackerInfantryType; + + if (hijack != -1) + { + const auto pHijackerType = InfantryTypeClass::Array[hijack]; + moneyToGive += static_cast(pHijackerType->GetRefund(pPoorGuy->Owner, true) * multiplier); + } + + if (moneyToGive > 0) + { + pOwner->GiveMoney(moneyToGive); + + if (displayCash) + { + FlyingStrings::AddMoneyString(moneyToGive, pThis, pOwner, + pDelType->DisplaySoylentToHouses, pThis->Location, pDelType->DisplaySoylentOffset); + } + } + } + else + { + if (const auto pParasite = pPoorGuy->ParasiteEatingMe) + { + pParasite->ParasiteImUsing->SuppressionTimer.Start(50); + pParasite->ParasiteImUsing->ExitUnit(); + } + } + + // Handle gunner change. + auto const pTransportType = pTypeExt->OwnerObject(); + + if (pTransportType->Gunner) + { + if (auto const pFoot = abstract_cast(pThis)) + { + pFoot->RemoveGunner(pPoorGuy); + + if (auto pGunner = pFoot->Passengers.GetFirstPassenger()) + { + for (auto pNext = abstract_cast(pGunner->NextObject); pNext; pNext = abstract_cast(pNext->NextObject)) + pGunner = pNext; + + pFoot->ReceiveGunner(pGunner); + } + } + } + + auto const pSource = pDelType->DontScore ? nullptr : pThis; + pPoorGuy->KillPassengers(pSource); + pPoorGuy->RegisterDestruction(pSource); + pPoorGuy->UnInit(); + + // Handle extra power + if (auto const pBldType = abstract_cast(pTransportType)) + { + if (pBldType->ExtraPowerBonus || pBldType->ExtraPowerDrain) + pOwner->RecheckPower = true; + } + + this->PassengerDeletionTimer.Stop(); + } + } + else + { + this->PassengerDeletionTimer.Stop(); + } + } +} + +void TechnoExt::ExtData::UpdateTiberiumEater() +{ + const auto pEaterType = this->TypeExtData->TiberiumEaterType.get(); + + if (!pEaterType) + return; + + const int transDelay = pEaterType->TransDelay; + + if (transDelay && this->TiberiumEater_Timer.InProgress()) + return; + + const auto pThis = this->OwnerObject(); + const auto pOwner = pThis->Owner; + bool active = false; + const bool displayCash = pEaterType->Display && pThis->IsClearlyVisibleTo(HouseClass::CurrentPlayer); + int facing = pThis->PrimaryFacing.Current().GetFacing<8>(); + + if (facing >= 7) + facing = 0; + else + facing++; + + const int cellCount = static_cast(pEaterType->Cells.size()); + const int locationZ = pThis->Location.Z; + const int numOrePurifiers = pOwner->NumOrePurifiers; + const float cashMultiplier = pEaterType->CashMultiplier; + const float purifierBonus = RulesClass::Instance->PurifierBonus; + const bool animMove = pEaterType->AnimMove; + const auto displayToHouse = pEaterType->DisplayToHouse; + const auto amountPerCell = pEaterType->AmountPerCell; + const auto displayOffset = pEaterType->DisplayOffset; + const auto& animsAll = pEaterType->Anims; + auto* scenarioRandom = &ScenarioClass::Instance->Random; + + for (int idx = 0; idx < cellCount; idx++) + { + const auto& cellOffset = pEaterType->Cells[idx]; + const auto pos = TechnoExt::GetFLHAbsoluteCoords(pThis, CoordStruct { cellOffset.X, cellOffset.Y, 0 }, false); + const auto pCell = MapClass::Instance.TryGetCellAt(pos); + + if (!pCell) + continue; + + if (const int contained = pCell->GetContainedTiberiumValue()) + { + const int tiberiumIdx = pCell->GetContainedTiberiumIndex(); + const int tiberiumValue = TiberiumClass::Array[tiberiumIdx]->Value; + const int tiberiumAmount = static_cast(static_cast(contained) / tiberiumValue); + const int amount = amountPerCell > 0 ? std::min(amountPerCell.Get(), tiberiumAmount) : tiberiumAmount; + pCell->ReduceTiberium(amount); + const float multiplier = cashMultiplier * (1.0f + numOrePurifiers * purifierBonus); + const int value = static_cast(std::round(amount * tiberiumValue * multiplier)); + pOwner->TransactMoney(value); + active = true; + + if (displayCash) + { + auto cellCoords = pCell->GetCoords(); + cellCoords.Z = std::max(locationZ, cellCoords.Z); + FlyingStrings::AddMoneyString(value, pThis, pOwner, displayToHouse, cellCoords, displayOffset); + } + + const auto& anims = pEaterType->Anims_Tiberiums[tiberiumIdx].GetElements(animsAll); + const int animCount = static_cast(anims.size()); + + if (animCount == 0) + continue; + + AnimTypeClass* pAnimType = nullptr; + + switch (animCount) + { + case 1: + pAnimType = anims[0]; + break; + + case 8: + pAnimType = anims[facing]; + break; + + default: + pAnimType = anims[scenarioRandom->RandomRanged(0, animCount - 1)]; + break; + } + + if (pAnimType) + { + const auto pAnim = GameCreate(pAnimType, pos); + AnimExt::SetAnimOwnerHouseKind(pAnim, pThis->Owner, nullptr, false, true); + + if (animMove) + pAnim->SetOwnerObject(pThis); + } + } + } + + if (active && transDelay) + this->TiberiumEater_Timer.Start(pEaterType->TransDelay); +} + +void TechnoExt::ExtData::UpdateShield() +{ + // Set current shield type if it is not set. + if (!this->CurrentShieldType || this->CurrentShieldType->Strength <= 0) + { + auto const pTypeExt = this->TypeExtData; + + if (pTypeExt->ShieldType && pTypeExt->ShieldType->Strength > 0) + this->CurrentShieldType = pTypeExt->ShieldType; + } + + // Create shield class instance if it does not exist. + if (this->CurrentShieldType && this->CurrentShieldType->Strength > 0 && !this->Shield) + { + this->Shield = std::make_unique(this->OwnerObject()); + this->Shield->UpdateTint(); + } + + if (const auto pShieldData = this->Shield.get()) + pShieldData->AI(); +} + +void TechnoExt::ExtData::UpdateOnTunnelEnter() +{ + if (!this->IsInTunnel) + { + if (const auto pShieldData = this->Shield.get()) + pShieldData->SetAnimationVisibility(false); + + for (const auto& pTrail : this->LaserTrails) + { + pTrail->Visible = false; + pTrail->LastLocation = { }; + } + + this->IsInTunnel = true; + } +} + +void TechnoExt::ExtData::UpdateOnTunnelExit() +{ + this->IsInTunnel = false; + + if (const auto pShieldData = this->Shield.get()) + pShieldData->SetAnimationVisibility(true); +} + +void TechnoExt::ExtData::ApplySpawnLimitRange() +{ + auto const pTypeExt = this->TypeExtData; + + if (pTypeExt->Spawner_LimitRange) + { + auto const pThis = this->OwnerObject(); + + if (auto const pManager = pThis->SpawnManager) + { + const int weaponRange = pThis->Veterancy.IsElite() ? pTypeExt->EliteSpawnerRange : pTypeExt->SpawnerRange; + + if (pManager->Target && (pThis->DistanceFrom(pManager->Target) > weaponRange)) + pManager->ResetTarget(); + } + } +} + +void TechnoExt::ExtData::UpdateTypeData(TechnoTypeClass* pCurrentType) +{ + auto const pThis = this->OwnerObject(); + auto const pOldType = this->TypeExtData->OwnerObject(); + auto const pOldTypeExt = TechnoTypeExt::ExtMap.Find(pOldType); + auto const pOwner = pThis->Owner; + auto& pSlaveManager = pThis->SlaveManager; + auto& pSpawnManager = pThis->SpawnManager; + auto& pCaptureManager = pThis->CaptureManager; + auto& pTemporalImUsing = pThis->TemporalImUsing; + auto& pAirstrike = pThis->Airstrike; + + // Cache the new type data + this->PreviousType = pOldType; + auto const pNewTypeExt = TechnoTypeExt::ExtMap.Find(pCurrentType); + this->TypeExtData = pNewTypeExt; + + this->UpdateSelfOwnedAttachEffects(); + + if (auto const pShield = this->Shield.get()) + pShield->ConvertCheck(pCurrentType); + + // Recalculate and redraw + this->UpdateTintValues(); + pThis->MarkForRedraw(); + + // Recreate Laser Trails + if (const size_t trailCount = this->LaserTrails.size()) + { + std::vector> addition; + addition.reserve(trailCount); + + for (auto& pTrail : this->LaserTrails) + { + if (!pTrail->Intrinsic) + addition.emplace_back(std::move(pTrail)); + } + + this->LaserTrails.clear(); + this->LaserTrails.reserve(this->TypeExtData->LaserTrailData.size() + addition.size()); + + for (const auto& entry : this->TypeExtData->LaserTrailData) + this->LaserTrails.emplace_back(std::make_unique(entry.GetType(), pOwner, entry.FLH, entry.IsOnTurret)); + + for (auto& pTrail : addition) + this->LaserTrails.emplace_back(std::move(pTrail)); + } + else if (const size_t trailSize = pNewTypeExt->LaserTrailData.size()) + { + this->LaserTrails.reserve(trailSize); + + for (const auto& entry : pNewTypeExt->LaserTrailData) + this->LaserTrails.emplace_back(std::make_unique(entry.GetType(), pOwner, entry.FLH, entry.IsOnTurret)); + } + + // Reset AutoDeath Timer if new techno type doesn't have timed AutoDeath + if (this->AutoDeathTimer.HasStarted() && pNewTypeExt->AutoDeath_AfterDelay <= 0) + this->AutoDeathTimer.Stop(); + + // Reset PassengerDeletion Timer + if (this->PassengerDeletionTimer.HasStarted() && pNewTypeExt->PassengerDeletionType && pNewTypeExt->PassengerDeletionType->Rate <= 0) + this->PassengerDeletionTimer.Stop(); + + // Remove from tracked AutoDeath objects if no longer has AutoDeath + if (pOldTypeExt->AutoDeath_Behavior.isset() && !pNewTypeExt->AutoDeath_Behavior.isset()) + { + auto& vec = ScenarioExt::Global()->AutoDeathObjects; + vec.erase(std::remove(vec.begin(), vec.end(), this), vec.end()); + } + + // Remove from harvesters list if no longer a harvester. + if (pOldTypeExt->Harvester_Counted) + { + if (!pNewTypeExt->Harvester_Counted) + { + auto& vec = HouseExt::ExtMap.Find(pOwner)->OwnedCountedHarvesters; + vec.erase(std::remove(vec.begin(), vec.end(), pThis), vec.end()); + } + } + // Add to harvesters list if it's a harvester. + else if (pNewTypeExt->Harvester_Counted) + { + HouseExt::ExtMap.Find(pOwner)->OwnedCountedHarvesters.push_back(pThis); + } + + // Remove from limbo reloaders if no longer applicable + if (pOldType->Ammo > 0 && pOldTypeExt->ReloadInTransport && !pNewTypeExt->ReloadInTransport) + { + auto& vec = ScenarioExt::Global()->TransportReloaders; + vec.erase(std::remove(vec.begin(), vec.end(), this), vec.end()); + } + + // Powered by ststl-s, Fly-Star + if (pCurrentType->Enslaves && pCurrentType->SlavesNumber > 0) + { + // SlaveManager does not exist or they have different slaves. + if (!pSlaveManager || pSlaveManager->SlaveType != pCurrentType->Enslaves) + { + if (pSlaveManager) + { + // Slaves are not the same, so clear out. + pSlaveManager->Killed(nullptr); + GameDelete(pSlaveManager); + pSlaveManager = nullptr; + } + + pSlaveManager = GameCreate(pThis, pCurrentType->Enslaves, pCurrentType->SlavesNumber, pCurrentType->SlaveRegenRate, pCurrentType->SlaveReloadRate); + } + else if (pSlaveManager->SlaveCount != pCurrentType->SlavesNumber) + { + // Additions/deletions made when quantities are inconsistent. + if (pSlaveManager->SlaveCount < pCurrentType->SlavesNumber) + { + // There are too few slaves here. More are needed. + const int count = pCurrentType->SlavesNumber - pSlaveManager->SlaveCount; + + for (int i = 0; i < count; i++) + { + const auto pSlaveNode = GameCreate(); + pSlaveNode->Slave = nullptr; + pSlaveNode->State = SlaveControlStatus::Dead; + pSlaveNode->RespawnTimer.Start(pCurrentType->SlaveRegenRate); + pSlaveManager->SlaveNodes.AddItem(pSlaveNode); + } + } + else + { + // Remove excess slaves + for (int i = pSlaveManager->SlaveCount - 1; i >= pCurrentType->SlavesNumber; --i) + { + if (const auto pSlaveNode = pSlaveManager->SlaveNodes.GetItem(i)) + { + if (const auto pSlave = pSlaveNode->Slave) + { + if (pSlave->InLimbo) + { + // He wasn't killed, just erased. + pSlave->RegisterDestruction(pThis); + pSlave->UnInit(); + } + else + { + // Oh, my God, he's been killed. + pSlave->ReceiveDamage(&pSlave->Health, 0, RulesClass::Instance->C4Warhead, nullptr, true, false, nullptr); + } + } + + // Unlink + pSlaveNode->Slave = nullptr; + pSlaveNode->State = SlaveControlStatus::Dead; + GameDelete(pSlaveNode); + } + + // Remove it + pSlaveManager->SlaveNodes.RemoveItem(i); + } + } + + pSlaveManager->SlaveCount = pCurrentType->SlavesNumber; + } + } + else if (pSlaveManager) + { + pSlaveManager->Killed(nullptr); + GameDelete(pSlaveManager); + pSlaveManager = nullptr; + } + + if (pCurrentType->Spawns && pCurrentType->SpawnsNumber > 0) + { + // No SpawnManager exists, or their SpawnType is inconsistent. + if (!pSpawnManager || pCurrentType->Spawns != pSpawnManager->SpawnType) + { + if (pSpawnManager) + { + // It may be odd that AircraftType is different, I chose to reset it. + pSpawnManager->KillNodes(); + GameDelete(pSpawnManager); + } + + pSpawnManager = GameCreate(pThis, pCurrentType->Spawns, pCurrentType->SpawnsNumber, pCurrentType->SpawnRegenRate, pCurrentType->SpawnReloadRate); + } + else if (pSpawnManager->SpawnCount != pCurrentType->SpawnsNumber) + { + // Additions/deletions made when quantities are inconsistent. + if (pSpawnManager->SpawnCount < pCurrentType->SpawnsNumber) + { + const int count = pCurrentType->SpawnsNumber - pSpawnManager->SpawnCount; + + // Add the missing Spawns, but don't intend for them to be born right away. + for (int i = 0; i < count; i++) + { + const auto pSpawnNode = GameCreate(); + pSpawnNode->Unit = nullptr; + pSpawnNode->Status = SpawnNodeStatus::Dead; + pSpawnNode->SpawnTimer.Start(pCurrentType->SpawnRegenRate); + pSpawnNode->IsSpawnMissile = false; + pSpawnManager->SpawnedNodes.AddItem(pSpawnNode); + } + } + else + { + // Remove excess spawns + for (int i = pSpawnManager->SpawnCount - 1; i >= pCurrentType->SpawnsNumber; --i) + { + if (const auto pSpawnNode = pSpawnManager->SpawnedNodes.GetItem(i)) + { + auto& pStatus = pSpawnNode->Status; + + // Spawns that don't die get killed. + if (const auto pAircraft = pSpawnNode->Unit) + { + pAircraft->SpawnOwner = nullptr; + + if (pAircraft->InLimbo + || pStatus == SpawnNodeStatus::Idle + || pStatus == SpawnNodeStatus::Reloading + || pStatus == SpawnNodeStatus::TakeOff) + { + if (pStatus == SpawnNodeStatus::TakeOff) + Kamikaze::Instance.Remove(pAircraft); + + pAircraft->UnInit(); + } + else if (pSpawnNode->IsSpawnMissile) + { + pAircraft->ReceiveDamage(&pAircraft->Health, 0, RulesClass::Instance->C4Warhead, nullptr, true, false, nullptr); + } + else + { + pAircraft->Crash(nullptr); + } + } + + // Unlink + pSpawnNode->Unit = nullptr; + pStatus = SpawnNodeStatus::Dead; + GameDelete(pSpawnNode); + } + + // Remove it + pSpawnManager->SpawnedNodes.RemoveItem(i); + } + } + + pSpawnManager->SpawnCount = pCurrentType->SpawnsNumber; + } + } + else if (pSpawnManager) + { + // Reset the target. + pSpawnManager->ResetTarget(); + + // pSpawnManager->KillNodes() kills all Spawns, but it is not necessary to kill the parts that are not performing tasks. + for (const auto pSpawnNode : pSpawnManager->SpawnedNodes) + { + const auto pAircraft = pSpawnNode->Unit; + auto& pStatus = pSpawnNode->Status; + + // A dead or idle Spawn is not killed. + if (!pAircraft + || pStatus == SpawnNodeStatus::Dead + || pStatus == SpawnNodeStatus::Idle + || pStatus == SpawnNodeStatus::Reloading) + { + continue; + } + + pAircraft->SpawnOwner = nullptr; + + if (pStatus == SpawnNodeStatus::TakeOff) + { + Kamikaze::Instance.Remove(pAircraft); + pAircraft->UnInit(); + } + else if (pSpawnNode->IsSpawnMissile) + { + pAircraft->ReceiveDamage(&pAircraft->Health, 0, RulesClass::Instance->C4Warhead, nullptr, true, false, nullptr); + } + else + { + pAircraft->Crash(nullptr); + } + + pSpawnNode->Unit = nullptr; + pStatus = SpawnNodeStatus::Dead; + pSpawnNode->IsSpawnMissile = false; + pSpawnNode->SpawnTimer.Start(pSpawnManager->RegenRate); + } + } + + // Prepare the variables. + int maxCapture = 0; + bool infiniteCapture = false; + bool hasTemporal = false; + bool hasAirstrike = false; + bool hasLocomotor = false; + bool hasParasite = false; + + auto checkWeapon = [&maxCapture, &infiniteCapture, &hasTemporal, + &hasAirstrike, &hasLocomotor, &hasParasite](WeaponTypeClass* pWeaponType) + { + if (!pWeaponType) + return; + + const auto pWH = pWeaponType->Warhead; + + if (pWH->MindControl) + { + if (pWeaponType->Damage > maxCapture) + maxCapture = pWeaponType->Damage; + + if (pWeaponType->InfiniteMindControl) + infiniteCapture = true; + } + + if (pWH->Temporal) + hasTemporal = true; + + if (pWH->Airstrike) + hasAirstrike = true; + + if (pWH->IsLocomotor) + hasLocomotor = true; + + if (pWH->Parasite) + hasParasite = true; + }; + + for (int i = 0; i < TechnoTypeClass::MaxWeapons; i++) + { + checkWeapon(pThis->GetWeapon(i)->WeaponType); + } + + if (maxCapture > 0) + { + if (!pCaptureManager) + { + // Rebuild a CaptureManager + pCaptureManager = GameCreate(pThis, maxCapture, infiniteCapture); + } + else if (pOldTypeExt->Convert_ResetMindControl) + { + if (!infiniteCapture && pCaptureManager->GetControlledCount() > maxCapture) + { + // Remove excess nodes. + for (int i = pCaptureManager->ControlNodes.Count - 1; i >= maxCapture; --i) + { + auto const pControlNode = pCaptureManager->ControlNodes.GetItem(i); + pCaptureManager->FreeUnit(pControlNode->Unit); + } + } + + pCaptureManager->MaxControlNodes = maxCapture; + pCaptureManager->InfiniteMindControl = infiniteCapture; + } + } + else if (pCaptureManager && pOldTypeExt->Convert_ResetMindControl) + { + // Remove CaptureManager completely + pCaptureManager->FreeAll(); + GameDelete(pCaptureManager); + pCaptureManager = nullptr; + } + + if (hasTemporal) + { + if (!pTemporalImUsing) + { + // Rebuild a TemporalClass + pTemporalImUsing = GameCreate(pThis); + } + } + else if (pTemporalImUsing) + { + if (pTemporalImUsing->Target) + { + // Free this afflicted man. + pTemporalImUsing->LetGo(); + } + + // Delete it + GameDelete(pTemporalImUsing); + pTemporalImUsing = nullptr; + } + + if (hasAirstrike && pCurrentType->AirstrikeTeam > 0) + { + if (!pAirstrike) + { + // Rebuild a AirstrikeClass + pAirstrike = GameCreate(pThis); + } + else + { + // Modify the parameters of AirstrikeClass. + pAirstrike->AirstrikeTeam = pCurrentType->AirstrikeTeam; + pAirstrike->EliteAirstrikeTeam = pCurrentType->EliteAirstrikeTeam; + pAirstrike->AirstrikeTeamType = pCurrentType->AirstrikeTeamType; + pAirstrike->EliteAirstrikeTeamType = pCurrentType->EliteAirstrikeTeamType; + pAirstrike->AirstrikeRechargeTime = pCurrentType->AirstrikeRechargeTime; + pAirstrike->EliteAirstrikeRechargeTime = pCurrentType->EliteAirstrikeRechargeTime; + } + } + else if (pAirstrike) + { + pAirstrike->InvalidatePointer(pThis); + GameDelete(pAirstrike); + pAirstrike = nullptr; + } + + if (!hasLocomotor && pThis->LocomotorTarget) + { + pThis->ReleaseLocomotor(pThis->Target == pThis->LocomotorTarget); + pThis->LocomotorTarget->LocomotorSource = nullptr; + pThis->LocomotorTarget = nullptr; + } + + // FireAngle + pThis->BarrelFacing.SetCurrent(DirStruct(0x4000 - (pCurrentType->FireAngle << 8))); + + // Reset recoil data + { + auto& turretRecoil = pThis->TurretRecoil.Turret; + const auto& turretAnimData = pCurrentType->TurretAnimData; + turretRecoil.Travel = turretAnimData.Travel; + turretRecoil.CompressFrames = turretAnimData.CompressFrames; + turretRecoil.RecoverFrames = turretAnimData.RecoverFrames; + turretRecoil.HoldFrames = turretAnimData.HoldFrames; + auto& barrelRecoil = pThis->BarrelRecoil.Turret; + const auto& barrelAnimData = pCurrentType->BarrelAnimData; + barrelRecoil.Travel = barrelAnimData.Travel; + barrelRecoil.CompressFrames = barrelAnimData.CompressFrames; + barrelRecoil.RecoverFrames = barrelAnimData.RecoverFrames; + barrelRecoil.HoldFrames = barrelAnimData.HoldFrames; + } + + if (pOldType->BombSight && !pCurrentType->BombSight) + BombListClass::Instance.RemoveDetector(pThis); + else if (!pOldType->BombSight && pCurrentType->BombSight) + BombListClass::Instance.AddDetector(pThis); + + // Only FootClass* can use this. + if (const auto pFoot = abstract_cast(pThis)) + { + auto& pParasiteImUsing = pFoot->ParasiteImUsing; + + if (hasParasite) + { + if (!pParasiteImUsing) + { + // Rebuild a ParasiteClass + pParasiteImUsing = GameCreate(pFoot); + } + } + else if (pParasiteImUsing) + { + if (pParasiteImUsing->Victim) + { + // Release of victims. + pParasiteImUsing->ExitUnit(); + } + + // Delete it + GameDelete(pParasiteImUsing); + pParasiteImUsing = nullptr; + } + } + + // handle AutoTargetOwnPosition + if (pOldTypeExt->AutoTargetOwnPosition && !pNewTypeExt->AutoTargetOwnPosition) + pThis->SetTarget(nullptr); + + // Clear AlphaImage + if (const auto pAlphaMap = AresFunctions::AlphaExtMap) + { + if (const auto pAlpha = pAlphaMap->get_or_default(pThis)) + GameDelete(pAlpha); + } +} + +void TechnoExt::ExtData::UpdateTypeData_Foot() +{ + auto const pThis = static_cast(this->OwnerObject()); + auto const pOldType = this->PreviousType; + auto const pCurrentType = this->TypeExtData->OwnerObject(); + auto const abs = pThis->WhatAmI(); + //auto const pOldTypeExt = TechnoTypeExt::ExtMap.Find(pOldType); + + // Update movement sound if still moving while type changed. + if (pThis->IsMoveSoundPlaying && pThis->Locomotor->Is_Moving()) + { + if (pCurrentType->MoveSound != pOldType->MoveSound) + { + // End the old sound. + pThis->MoveSoundAudioController.End(); + + if (auto const count = pCurrentType->MoveSound.Count) + { + // Play a new sound. + const int soundIndex = pCurrentType->MoveSound[Randomizer::Global.Random() % count]; + VocClass::PlayAt(soundIndex, pThis->Location, &pThis->MoveSoundAudioController); + pThis->IsMoveSoundPlaying = true; + } + else + { + pThis->IsMoveSoundPlaying = false; + } + + pThis->MoveSoundDelay = 0; + } + } + + if (abs == AbstractType::Infantry) + { + auto const pInf = static_cast(pThis); + + // It's still not recommended to have such idea, please avoid using this + if (static_cast(pOldType)->Deployer && !static_cast(pCurrentType)->Deployer) + { + switch (pInf->SequenceAnim) + { + case Sequence::Deploy: + case Sequence::Deployed: + case Sequence::DeployedIdle: + pInf->PlayAnim(Sequence::Ready, true); + break; + case Sequence::DeployedFire: + pInf->PlayAnim(Sequence::FireUp, true); + break; + default: + break; + } + } + } + + if (pOldType->Locomotor == LocomotionClass::CLSIDs::Teleport && pCurrentType->Locomotor != LocomotionClass::CLSIDs::Teleport && pThis->WarpingOut) + this->HasRemainingWarpInDelay = true; + + // Update open topped state of potential passengers if transport's OpenTopped value changes. + // OpenTopped does not work properly with buildings to begin with which is why this is here rather than in the Techno update one. + if (pThis->Passengers.NumPassengers > 0) + { + const bool toOpenTopped = pCurrentType->OpenTopped; + FootClass* pFirstPassenger = pThis->Passengers.GetFirstPassenger(); + + while (true) + { + if (toOpenTopped) + { + pFirstPassenger->SetLocation(pThis->Location); + // Add passengers to the logic layer. + pThis->EnteredOpenTopped(pFirstPassenger); + } + else + { + // Lose target & destination + pFirstPassenger->SetTarget(nullptr); + pFirstPassenger->SetCurrentWeaponStage(0); + pFirstPassenger->AbortMotion(); + pThis->ExitedOpenTopped(pFirstPassenger); + + // OpenTopped adds passengers to logic layer when enabled. Under normal conditions this does not need to be removed since + // OpenTopped state does not change while passengers are still in transport but in case of type conversion that can happen. + LogicClass::Instance.RemoveObject(pFirstPassenger); + } + + pFirstPassenger->Transporter = pThis; + + if (const auto pNextPassenger = abstract_cast(pFirstPassenger->NextObject)) + pFirstPassenger = pNextPassenger; + else + break; + } + + if (pCurrentType->Gunner) + pThis->ReceiveGunner(pFirstPassenger); + } + else if (pCurrentType->Gunner) + { + pThis->RemoveGunner(nullptr); + } + + if (!pCurrentType->CanDisguise || (!pThis->Disguise && pCurrentType->PermaDisguise)) + { + // When it can't disguise or has lost its disguise, update its disguise. + pThis->ClearDisguise(); + } + + if (abs != AbstractType::Aircraft) + { + auto const pLocomotorType = pCurrentType->Locomotor; + + // The Hover movement pattern allows for self-landing. + if (pLocomotorType != LocomotionClass::CLSIDs::Fly && pLocomotorType != LocomotionClass::CLSIDs::Hover) + { + const bool isinAir = pThis->IsInAir() && !pThis->LocomotorSource; + + if (auto const pJJLoco = locomotion_cast(pThis->Locomotor)) + { + const int turnrate = pCurrentType->JumpjetTurnRate >= 127 ? 127 : pCurrentType->JumpjetTurnRate; + pJJLoco->Speed = pCurrentType->JumpjetSpeed; + pJJLoco->Climb = pCurrentType->JumpjetClimb; + pJJLoco->Accel = pCurrentType->JumpjetAccel; + pJJLoco->Crash = pCurrentType->JumpjetCrash; + pJJLoco->Deviation = pCurrentType->JumpjetDeviation; + pJJLoco->NoWobbles = pCurrentType->JumpjetNoWobbles; + pJJLoco->Wobbles = pCurrentType->JumpjetWobbles; + pJJLoco->TurnRate = turnrate; + pJJLoco->CurrentHeight = pCurrentType->JumpjetHeight; + pJJLoco->Height = pCurrentType->JumpjetHeight; + pJJLoco->LocomotionFacing.SetROT(turnrate); + + if (isinAir) + { + if (pCurrentType->BalloonHover) + { + // Makes the jumpjet think it is hovering without actually moving. + pJJLoco->State = JumpjetLocomotionClass::State::Hovering; + pJJLoco->IsMoving = true; + + if (!pJJLoco->Is_Moving_Now()) + pJJLoco->DestinationCoords = pThis->Location; + } + else if (!pJJLoco->Is_Moving_Now()) + { + pJJLoco->Move_To(pThis->Location); + } + } + } + else if (isinAir) + { + // Let it go into free fall. + pThis->IsFallingDown = true; + + const auto pCell = MapClass::Instance.TryGetCellAt(pThis->Location); + + if (pCell && !pCell->IsClearToMove(pCurrentType->SpeedType, true, true, + -1, pCurrentType->MovementZone, pCell->GetLevel(), pCell->ContainsBridge())) + { + // If it's landing position cannot be moved, then it is granted a crash death. + pThis->IsABomb = true; + } + else + { + // If it's gonna land on the bridge, then it needs this. + pThis->OnBridge = pCell ? pCell->ContainsBridge() : false; + this->OnParachuted = true; + } + + if (abs == AbstractType::Infantry) + { + // Infantry changed to parachute status (not required). + static_cast(pThis)->PlayAnim(Sequence::Paradrop, true, false); + } + } + } + + if (abs == AbstractType::Unit) + { + // Yes, synchronize its turret facing or it will turn strangely. + if (pOldType->Turret != pCurrentType->Turret) + { + const auto primaryFacing = pThis->PrimaryFacing.Current(); + auto& secondaryFacing = pThis->SecondaryFacing; + + secondaryFacing.SetCurrent(primaryFacing); + secondaryFacing.SetDesired(primaryFacing); + } + } + } + + this->PreviousType = nullptr; +} + +void TechnoExt::ExtData::UpdateLaserTrails() +{ + if (this->LaserTrails.size() <= 0) + return; + + auto const pThis = this->OwnerObject(); + auto const pOwner = pThis->Owner; + auto const cloakState = pThis->CloakState; + + // LaserTrails update routine is in TechnoClass::AI hook because LaserDrawClass-es are updated in LogicClass::AI + for (const auto& pTrail : this->LaserTrails) + { + auto const pType = pTrail->Type; + + if (pType->DroppodOnly && (pThis->AbstractFlags & AbstractFlags::Foot) != AbstractFlags::None) + { + auto const pFoot = static_cast(pThis); + + // @Kerbiter if you want to limit it to certain locos you do it here + // // with vtable check you can avoid the tedious process of Query IPersit/IUnknown Interface, GetClassID, compare with loco GUID, which is omnipresent in vanilla code + if (VTable::Get(pFoot->Locomotor.GetInterfacePtr()) != 0x7E8278) + continue; + } + + pTrail->Cloaked = false; + + if (cloakState == CloakState::Cloaked) + { + if (pType->CloakVisible && pType->CloakVisible_DetectedOnly && !HouseClass::IsCurrentPlayerObserver() && !pOwner->IsAlliedWith(HouseClass::CurrentPlayer)) + pTrail->Cloaked = !pThis->GetCell()->Sensors_InclHouse(HouseClass::CurrentPlayer->ArrayIndex); + else if (!pType->CloakVisible) + pTrail->Cloaked = true; + } + + if (!this->IsInTunnel) + pTrail->Visible = true; + + auto const trailLoc = TechnoExt::GetFLHAbsoluteCoords(pThis, pTrail->FLH, pTrail->IsOnTurret); + + if (cloakState == CloakState::Uncloaking && !pType->CloakVisible) + pTrail->LastLocation = trailLoc; + else + pTrail->Update(trailLoc); + } +} + +void TechnoExt::ExtData::UpdateMindControlAnim() +{ + auto const pThis = this->OwnerObject(); + + if (pThis->IsMindControlled()) + { + if (pThis->MindControlRingAnim && !this->MindControlRingAnimType) + { + this->MindControlRingAnimType = pThis->MindControlRingAnim->Type; + } + else if (!pThis->MindControlRingAnim && this->MindControlRingAnimType + && pThis->CloakState == CloakState::Uncloaked && !pThis->InLimbo && pThis->IsAlive) + { + auto coords = pThis->GetCoords(); + int offset = 0; + + if (const auto pBuilding = specific_cast(pThis)) + offset = Unsorted::LevelHeight * pBuilding->Type->Height; + else + offset = pThis->GetTechnoType()->MindControlRingOffset; + + coords.Z += offset; + pThis->MindControlRingAnim = GameCreate(this->MindControlRingAnimType, coords, 0, 1); + pThis->MindControlRingAnim->SetOwnerObject(pThis); + + if (pThis->WhatAmI() == AbstractType::Building) + pThis->MindControlRingAnim->ZAdjust = -1024; + } + } + else if (this->MindControlRingAnimType) + { + this->MindControlRingAnimType = nullptr; + } +} + +void TechnoExt::ExtData::UpdateRecountBurst() +{ + const auto pThis = this->OwnerObject(); + + if (pThis->CurrentBurstIndex && !pThis->Target && this->TypeExtData->RecountBurst.Get(RulesExt::Global()->RecountBurst)) + { + const auto pWeapon = this->LastWeaponType; + + if (pWeapon && pWeapon->Burst && pThis->LastFireBulletFrame + std::max(pWeapon->ROF, 30) <= Unsorted::CurrentFrame) + { + const auto ratio = static_cast(pThis->CurrentBurstIndex) / pWeapon->Burst; + const auto rof = static_cast(ratio * pWeapon->ROF * this->AE.ROFMultiplier) - std::max(pWeapon->ROF, 30); + + if (rof > 0) + { + pThis->ChargeTurretDelay = rof; + pThis->RearmTimer.Start(rof); + } + + pThis->CurrentBurstIndex = 0; + } + } +} + +void TechnoExt::ExtData::UpdateGattlingRateDownReset() +{ + const auto pTypeExt = this->TypeExtData; + + if (pTypeExt->OwnerObject()->IsGattling) + { + const auto pThis = this->OwnerObject(); + + if (pTypeExt->RateDown_Reset && (!pThis->Target || this->LastTargetID != pThis->Target->UniqueID)) + { + const int oldStage = pThis->CurrentGattlingStage; + this->LastTargetID = pThis->Target ? pThis->Target->UniqueID : 0xFFFFFFFF; + pThis->GattlingValue = 0; + pThis->CurrentGattlingStage = 0; + this->AccumulatedGattlingValue = 0; + this->ShouldUpdateGattlingValue = false; + + if (oldStage != 0) + pThis->GattlingRateDown(0); + } + } +} + +void TechnoExt::ApplyGainedSelfHeal(TechnoClass* pThis) +{ + if (!RulesExt::Global()->GainSelfHealAllowMultiplayPassive && pThis->Owner->Type->MultiplayPassive) + return; + + auto const pTypeExt = TechnoExt::ExtMap.Find(pThis)->TypeExtData; + auto const pType = pTypeExt->OwnerObject(); + const int healthDeficit = pType->Strength - pThis->Health; + + if (pThis->Health && healthDeficit > 0) + { + auto defaultSelfHealType = SelfHealGainType::NoHeal; + auto const whatAmI = pThis->WhatAmI(); + + if (whatAmI == AbstractType::Infantry) + defaultSelfHealType = SelfHealGainType::Infantry; + else if (whatAmI == AbstractType::Unit) + defaultSelfHealType = (pType->Organic ? SelfHealGainType::Infantry : SelfHealGainType::Units); + + auto const selfHealType = pTypeExt->SelfHealGainType.Get(defaultSelfHealType); + + if (selfHealType == SelfHealGainType::NoHeal) + return; + + if ((selfHealType == SelfHealGainType::Infantry) + ? (Unsorted::CurrentFrame % RulesClass::Instance->SelfHealInfantryFrames) + : (Unsorted::CurrentFrame % RulesClass::Instance->SelfHealUnitFrames)) + { + return; + } + + int amount = 0; + + auto countSelfHealing = [pThis](const bool infantryHeal) + { + auto const pOwner = pThis->Owner; + const bool hasCap = infantryHeal ? RulesExt::Global()->InfantryGainSelfHealCap.isset() : RulesExt::Global()->UnitsGainSelfHealCap.isset(); + const int cap = std::max(infantryHeal ? RulesExt::Global()->InfantryGainSelfHealCap.Get() : RulesExt::Global()->UnitsGainSelfHealCap.Get(), 1); + + auto healCount = [infantryHeal](HouseClass* pHouse) + { + return (infantryHeal ? pHouse->InfantrySelfHeal : pHouse->UnitsSelfHeal); + }; + int count = healCount(pOwner); + + if (hasCap && count >= cap) + return cap; + + const bool isCampaign = SessionClass::IsCampaign(); + const bool fromPlayer = RulesExt::Global()->GainSelfHealFromPlayerControl && isCampaign && (pOwner->IsHumanPlayer || pOwner->IsInPlayerControl); + const bool fromAllies = RulesExt::Global()->GainSelfHealFromAllies; + + if (fromPlayer || fromAllies) + { + auto checkHouse = [fromPlayer, fromAllies, isCampaign, pOwner](HouseClass* pHouse) + { + if (pHouse == pOwner) + return false; + + return (fromPlayer && (pHouse->IsHumanPlayer || pHouse->IsInPlayerControl)) // pHouse->IsControlledByCurrentPlayer() + || (fromAllies && (!isCampaign || (!pHouse->IsHumanPlayer && !pHouse->IsInPlayerControl)) && pHouse->IsAlliedWith(pOwner)); + }; + + for (auto const pHouse : HouseClass::Array) + { + if (checkHouse(pHouse)) + { + count += healCount(pHouse); + + if (hasCap && count >= cap) + return cap; + } + } + } + + return count; + }; + + if (selfHealType == SelfHealGainType::Infantry) + amount = RulesClass::Instance->SelfHealInfantryAmount * countSelfHealing(true); + else + amount = RulesClass::Instance->SelfHealUnitAmount * countSelfHealing(false); + + if (amount) + { + if (amount >= healthDeficit) + amount = healthDeficit; + + const bool wasDamaged = pThis->GetHealthPercentage() <= RulesClass::Instance->ConditionYellow; + + pThis->Health += amount; + + if (wasDamaged && (pThis->GetHealthPercentage() > RulesClass::Instance->ConditionYellow + || pThis->GetHeight() < -10)) + { + if (auto const pBuilding = abstract_cast(pThis)) + { + pBuilding->Mark(MarkType::Change); + pBuilding->ToggleDamagedAnims(false); + } + + auto const dmgParticle = pThis->DamageParticleSystem; + + if (dmgParticle) + dmgParticle->UnInit(); + } + } + } + + return; +} + +void TechnoExt::ExtData::ApplyMindControlRangeLimit() +{ + auto const pThis = this->OwnerObject(); + + if (auto const pCapturer = pThis->MindControlledBy) + { + auto const pCapturerExt = TechnoExt::ExtMap.Find(pCapturer)->TypeExtData; + + if (pCapturerExt->MindControlRangeLimit.Get() > 0 + && pCapturer->DistanceFrom(pThis) > pCapturerExt->MindControlRangeLimit.Get()) + { + pCapturer->CaptureManager->FreeUnit(pThis); + } + } +} + +void TechnoExt::KillSelf(TechnoClass* pThis, AutoDeathBehavior deathOption, const std::vector& pVanishAnimation, bool isInLimbo) +{ + if (isInLimbo) + { + // Remove parasite units first before deleting them. + if (auto const pFoot = abstract_cast(pThis)) + { + if (pFoot->ParasiteImUsing && pFoot->ParasiteImUsing->Victim) + pFoot->ParasiteImUsing->ExitUnit(); + } + + // Remove limbo buildings' tracking here because their are not truely InLimbo + if (auto const pBuilding = abstract_cast(pThis)) + { + auto const pBldType = pBuilding->Type; + + if (!pBuilding->InLimbo && !pBldType->Insignificant && !pBldType->DontScore) + HouseExt::ExtMap.Find(pBuilding->Owner)->RemoveFromLimboTracking(pBldType); + } + + auto const pTransport = pThis->Transporter; + + // Handle extra power + if (pTransport && pThis->Absorbed) + pTransport->Owner->RecheckPower = true; + + pThis->RegisterKill(pThis->Owner); + pThis->UnInit(); + + // Handle gunner change. + if (auto const pTransportFoot = abstract_cast(pTransport)) + { + if (pTransportFoot->GetTechnoType()->Gunner) + { + pTransportFoot->RemoveGunner(nullptr); + + if (auto pGunner = pTransportFoot->Passengers.GetFirstPassenger()) + { + for (auto pNext = abstract_cast(pGunner->NextObject); pNext; pNext = abstract_cast(pNext->NextObject)) + pGunner = pNext; + + pTransportFoot->ReceiveGunner(pGunner); + } + } + } + + return; + } + + switch (deathOption) + { + + case AutoDeathBehavior::Vanish: + { + AnimExt::CreateRandomAnim(pVanishAnimation, pThis->GetCoords(), pThis, nullptr, true); + + if (const auto pBuilding = abstract_cast(pThis)) + { + if (pThis->BunkerLinkedItem) + pBuilding->UnloadBunker(); + } + + pThis->KillPassengers(pThis); + pThis->Stun(); + pThis->Limbo(); + pThis->RegisterKill(pThis->Owner); + pThis->UnInit(); + + return; + } + + case AutoDeathBehavior::Sell: + { + if (auto const pBld = abstract_cast(pThis)) + { + if (pBld->HasBuildUp) + { + // Sorry FirestormWall + if (pBld->GetCurrentMission() != Mission::Selling) + { + pBld->QueueMission(Mission::Selling, false); + pBld->NextMission(); + } + return; + } + } + if (Phobos::Config::DevelopmentCommands) + Debug::Log("[Developer warning] AutoDeath: [%s] can't be sold, killing it instead\n", pThis->get_ID()); + } + + default: //must be AutoDeathBehavior::Kill + if (AresFunctions::SpawnSurvivors) + { + switch (pThis->WhatAmI()) + { + case AbstractType::Unit: + case AbstractType::Aircraft: + AresFunctions::SpawnSurvivors(static_cast(pThis), nullptr, false, false); + default:; + } + } + pThis->ReceiveDamage(&pThis->Health, 0, RulesClass::Instance->C4Warhead, nullptr, true, false, nullptr); + return; + } +} + +void TechnoExt::UpdateSharedAmmo(TechnoClass* pThis) +{ + const auto pExt = TechnoExt::ExtMap.Find(pThis)->TypeExtData; + + if (pExt->Ammo_Shared) + { + const auto pType = pExt->OwnerObject(); + + if (pType->OpenTopped && pType->Ammo > 0) + { + for (auto pPassenger = pThis->Passengers.GetFirstPassenger(); pPassenger; pPassenger = abstract_cast(pPassenger->NextObject)) + { + const auto pPassengerExt = TechnoExt::ExtMap.Find(pPassenger)->TypeExtData; + + if (pPassengerExt->Ammo_Shared) + { + if (pExt->Ammo_Shared_Group < 0 || pExt->Ammo_Shared_Group == pPassengerExt->Ammo_Shared_Group) + { + if (pThis->Ammo > 0 && (pPassenger->Ammo < pPassengerExt->OwnerObject()->Ammo)) + { + pThis->Ammo--; + pPassenger->Ammo++; + } + } + } + } + } + } +} + +void TechnoExt::ExtData::UpdateTemporal() +{ + if (const auto pShieldData = this->Shield.get()) + { + if (pShieldData->IsAvailable()) + pShieldData->AI_Temporal(); + } + + for (auto const& ae : this->AttachedEffects) + ae->AI_Temporal(); + + this->UpdateRearmInTemporal(); +} + +void TechnoExt::ExtData::UpdateRearmInEMPState() +{ + const auto pThis = this->OwnerObject(); + + if (!pThis->IsUnderEMP() && !pThis->Deactivated) + return; + + const auto pTypeExt = this->TypeExtData; + + if (pThis->RearmTimer.InProgress() && pTypeExt->NoRearm_UnderEMP.Get(RulesExt::Global()->NoRearm_UnderEMP)) + pThis->RearmTimer.StartTime++; + + if (pThis->ReloadTimer.InProgress() && pTypeExt->NoReload_UnderEMP.Get(RulesExt::Global()->NoReload_UnderEMP)) + pThis->ReloadTimer.StartTime++; +} + +void TechnoExt::ExtData::UpdateRearmInTemporal() +{ + const auto pThis = this->OwnerObject(); + const auto pTypeExt = this->TypeExtData; + + if (pThis->RearmTimer.InProgress() && pTypeExt->NoRearm_Temporal.Get(RulesExt::Global()->NoRearm_Temporal)) + pThis->RearmTimer.StartTime++; + + if (pThis->ReloadTimer.InProgress() && pTypeExt->NoReload_Temporal.Get(RulesExt::Global()->NoReload_Temporal)) + pThis->ReloadTimer.StartTime++; +} + +// Resets target if KeepTargetOnMove unit moves beyond weapon range. +void TechnoExt::ExtData::UpdateKeepTargetOnMove() +{ + if (!this->KeepTargetOnMove) + return; + + auto const pThis = this->OwnerObject(); + + if (!pThis->Target) + { + this->KeepTargetOnMove = false; + return; + } + + const auto pTypeExt = this->TypeExtData; + + if (!pTypeExt->KeepTargetOnMove) + { + pThis->SetTarget(nullptr); + this->KeepTargetOnMove = false; + return; + } + + if (pThis->CurrentMission == Mission::Guard) + { + if (!pTypeExt->KeepTargetOnMove_NoMorePursuit) + { + pThis->QueueMission(Mission::Attack, false); + this->KeepTargetOnMove = false; + return; + } + } + else if (pThis->CurrentMission != Mission::Move) + { + return; + } + + const int weaponIndex = pTypeExt->KeepTargetOnMove_Weapon >= 0 ? pTypeExt->KeepTargetOnMove_Weapon : pThis->SelectWeapon(pThis->Target); + + if (auto const pWeapon = pThis->GetWeapon(weaponIndex)->WeaponType) + { + const int extraDistance = static_cast(pTypeExt->KeepTargetOnMove_ExtraDistance.Get()); + const int range = pWeapon->Range; + pWeapon->Range += extraDistance; // Temporarily adjust weapon range based on the extra distance. + + if (!pThis->IsCloseEnough(pThis->Target, weaponIndex)) + { + pThis->SetTarget(nullptr); + this->KeepTargetOnMove = false; + } + + pWeapon->Range = range; + } +} + +void TechnoExt::ExtData::UpdateWarpInDelay() +{ + if (this->HasRemainingWarpInDelay) + { + if (this->LastWarpInDelay) + { + this->LastWarpInDelay--; + } + else + { + this->HasRemainingWarpInDelay = false; + this->IsBeingChronoSphered = false; + this->OwnerObject()->WarpingOut = false; + } + } +} + +// Updates state of all AttachEffects on techno. +void TechnoExt::ExtData::UpdateAttachEffects() +{ + if (!this->AttachedEffects.size()) + return; + + auto const pThis = this->OwnerObject(); + const bool inTunnel = this->IsInTunnel || this->IsBurrowed; + bool markForRedraw = false; + bool altered = false; + std::vector>::iterator it; + std::vector> expireWeapons; + + for (it = this->AttachedEffects.begin(); it != this->AttachedEffects.end(); ) + { + auto const attachEffect = it->get(); + + if (!inTunnel) + attachEffect->SetAnimationTunnelState(true); + + attachEffect->AI(); + + if (attachEffect->NeedsRecalculateStat) + { + altered = true; + attachEffect->NeedsRecalculateStat = false; + } + + const bool hasExpired = attachEffect->HasExpired(); + const bool shouldDiscard = attachEffect->IsActiveIgnorePowered() && attachEffect->ShouldBeDiscardedNow(); + + if (hasExpired || shouldDiscard) + { + auto const pType = attachEffect->GetType(); + attachEffect->ShouldBeDiscarded = false; + + if (pType->HasTint()) + markForRedraw = true; + + if (pType->Cumulative && pType->CumulativeAnimations.size() > 0) + this->UpdateCumulativeAttachEffects(attachEffect->GetType(), attachEffect); + + if (pType->ExpireWeapon && ((hasExpired && (pType->ExpireWeapon_TriggerOn & ExpireWeaponCondition::Expire) != ExpireWeaponCondition::None) + || (shouldDiscard && (pType->ExpireWeapon_TriggerOn & ExpireWeaponCondition::Discard) != ExpireWeaponCondition::None))) + { + if (!pType->Cumulative || !pType->ExpireWeapon_CumulativeOnlyOnce || this->GetAttachedEffectCumulativeCount(pType) < 1) + { + if (pType->ExpireWeapon_UseInvokerAsOwner) + { + if (auto const pInvoker = attachEffect->GetInvoker()) + expireWeapons.push_back(std::make_pair(pType->ExpireWeapon, pInvoker)); + } + else + { + expireWeapons.push_back(std::make_pair(pType->ExpireWeapon, pThis)); + } + } + } + + // Next AE chain + if (hasExpired && !pType->Next.empty()) + { + auto const pInvoker = attachEffect->GetInvoker(); + auto const pInvokerHouse = attachEffect->InvokerHouse; + for (auto pNextType : pType->Next) + { + if (pNextType) + AttachEffectClass::CreateAndAttach(pNextType, pThis, pThis->GetTechnoType(), this->AttachedEffects, + pInvokerHouse ? pInvokerHouse : pThis->Owner, pInvoker ? pInvoker : pThis, attachEffect->Source, AEAttachParams{}); + } + altered = true; + } + + if (shouldDiscard && attachEffect->ResetIfRecreatable()) + { + ++it; + altered = true; + continue; + } + + it = this->AttachedEffects.erase(it); + altered = true; + } + else + { + ++it; + } + } + + if (altered) + this->RecalculateStatMultipliers(); + + if (markForRedraw) + pThis->MarkForRedraw(); + + auto const coords = pThis->GetCoords(); + + for (auto const& pair : expireWeapons) + { + auto const pInvoker = pair.second; + WeaponTypeExt::DetonateAt(pair.first, coords, pInvoker, pInvoker->Owner, pThis); + } +} + +// Updates self-owned (defined on TechnoType) AttachEffects, called on type conversion. +void TechnoExt::ExtData::UpdateSelfOwnedAttachEffects() +{ + auto const pThis = this->OwnerObject(); + auto const pTypeExt = this->TypeExtData; + auto const pTechnoType = pTypeExt->OwnerObject(); + std::vector>::iterator it; + std::vector> expireWeapons; + bool altered = false; + + // Delete ones on old type and not on current. + for (it = this->AttachedEffects.begin(); it != this->AttachedEffects.end(); ) + { + auto const attachEffect = it->get(); + auto const pType = attachEffect->GetType(); + const bool isValid = EnumFunctions::IsTechnoEligible(pThis, pType->AffectsTarget, true) + && (pType->AffectTypes.empty() || pType->AffectTypes.Contains(pTechnoType)) && !pType->IgnoreTypes.Contains(pTechnoType); + const bool remove = !isValid || (attachEffect->IsSelfOwned() && !pTypeExt->AttachEffects.AttachTypes.Contains(pType)); + + if (remove) + { + if (pType->ExpireWeapon && (pType->ExpireWeapon_TriggerOn & ExpireWeaponCondition::Expire) != ExpireWeaponCondition::None) + { + if (!pType->Cumulative || !pType->ExpireWeapon_CumulativeOnlyOnce || this->GetAttachedEffectCumulativeCount(pType) < 1) + { + if (pType->ExpireWeapon_UseInvokerAsOwner) + { + if (auto const pInvoker = attachEffect->GetInvoker()) + expireWeapons.push_back(std::make_pair(pType->ExpireWeapon, pInvoker)); + } + else + { + expireWeapons.push_back(std::make_pair(pType->ExpireWeapon, pThis)); + } + } + } + + it = this->AttachedEffects.erase(it); + altered = true; + } + else + { + it++; + } + } + + auto const coords = pThis->GetCoords(); + + for (auto const& pair : expireWeapons) + { + auto const pInvoker = pair.second; + WeaponTypeExt::DetonateAt(pair.first, coords, pInvoker, pInvoker->Owner, pThis); + } + + // Add new ones. + const int count = AttachEffectClass::Attach(pThis, pThis->Owner, pThis, pThis, pTypeExt->AttachEffects); + + if (altered && !count) + this->RecalculateStatMultipliers(); +} + +// Updates CumulativeAnimations AE's on techno. +void TechnoExt::ExtData::UpdateCumulativeAttachEffects(AttachEffectTypeClass* pAttachEffectType, AttachEffectClass* pRemoved) +{ + AttachEffectClass* pAELargestDuration = nullptr; + AttachEffectClass* pAEWithAnim = nullptr; + int duration = 0; + + for (auto const& attachEffect : this->AttachedEffects) + { + if (attachEffect->GetType() != pAttachEffectType) + continue; + + if (attachEffect->HasCumulativeAnim) + { + pAEWithAnim = attachEffect.get(); + } + else if (attachEffect->CanShowAnim()) + { + const int currentDuration = attachEffect->GetRemainingDuration(); + + if (currentDuration < 0 || currentDuration > duration) + { + pAELargestDuration = attachEffect.get(); + duration = currentDuration; + } + } + } + + if (pAEWithAnim) + { + pAEWithAnim->UpdateCumulativeAnim(); + + if (pRemoved == pAEWithAnim) + { + pAEWithAnim->HasCumulativeAnim = false; + + if (pAELargestDuration) + pAELargestDuration->TransferCumulativeAnim(pAEWithAnim); + } + } +} + +// Recalculates AttachEffect stat multipliers and other bonuses. +bool TechnoExt::ExtData::RecalculateStatMultipliers(AttachEffectClass* pAttachEffect) +{ + auto const pThis = this->OwnerObject(); + auto& pAE = this->AE; + + if (pAttachEffect) + { + auto const type = pAttachEffect->GetType(); + pAE.FirepowerMultiplier *= type->FirepowerMultiplier; + pAE.SpeedMultiplier *= type->SpeedMultiplier; + pAE.ROFMultiplier *= type->ROFMultiplier; + pAE.Cloakable |= type->Cloakable; + pAE.ForceDecloak |= type->ForceDecloak; + pAE.DisableWeapons |= type->DisableWeapons; + pAE.Unkillable |= type->Unkillable; + pAE.HasRangeModifier |= (type->WeaponRange_ExtraRange != 0.0 || type->WeaponRange_Multiplier != 0.0); + pAE.HasTint |= type->HasTint(); + pAE.ReflectDamage |= type->ReflectDamage; + pAE.HasOnFireDiscardables |= (type->DiscardOn & DiscardCondition::Firing) != DiscardCondition::None; + pAE.HasCritModifiers |= (type->Crit_Multiplier != 1.0 || type->Crit_ExtraChance != 0.0); + + if (type->ArmorMultiplier != 1.0 && (type->ArmorMultiplier_AllowWarheads.size() > 0 || type->ArmorMultiplier_DisallowWarheads.size() > 0)) + pAE.HasRestrictedArmorMultipliers = true; + else + pAE.ArmorMultiplier *= type->ArmorMultiplier; + + return pAE.ForceDecloak; + } + + const bool wasTint = pAE.HasTint; + double firepower = 1.0; + double armor = 1.0; + double speed = 1.0; + double ROF = 1.0; + bool cloak = false; + bool forceDecloak = false; + bool disableWeapons = false; + bool unkillable = false; + bool hasRangeModifier = false; + bool hasTint = false; + bool reflectsDamage = false; + bool hasOnFireDiscardables = false; + bool hasRestrictedArmorMultipliers = false; + bool hasCritModifiers = false; + + for (const auto& attachEffect : this->AttachedEffects) + { + if (!attachEffect->IsActive()) + continue; + + auto const type = attachEffect->GetType(); + firepower *= type->FirepowerMultiplier; + speed *= type->SpeedMultiplier; + + if (type->ArmorMultiplier != 1.0 && (type->ArmorMultiplier_AllowWarheads.size() > 0 || type->ArmorMultiplier_DisallowWarheads.size() > 0)) + hasRestrictedArmorMultipliers = true; + else + armor *= type->ArmorMultiplier; + + ROF *= type->ROFMultiplier; + cloak |= type->Cloakable; + forceDecloak |= type->ForceDecloak; + disableWeapons |= type->DisableWeapons; + unkillable |= type->Unkillable; + hasRangeModifier |= (type->WeaponRange_ExtraRange != 0.0 || type->WeaponRange_Multiplier != 0.0); + hasTint |= type->HasTint(); + reflectsDamage |= type->ReflectDamage; + hasOnFireDiscardables |= (type->DiscardOn & DiscardCondition::Firing) != DiscardCondition::None; + hasCritModifiers |= (type->Crit_Multiplier != 1.0 || type->Crit_ExtraChance != 0.0); + } + + pAE.FirepowerMultiplier = firepower; + pAE.ArmorMultiplier = armor; + pAE.SpeedMultiplier = speed; + pAE.ROFMultiplier = ROF; + pAE.Cloakable = cloak; + pAE.ForceDecloak = forceDecloak; + pAE.DisableWeapons = disableWeapons; + pAE.Unkillable = unkillable; + pAE.HasRangeModifier = hasRangeModifier; + pAE.HasTint = hasTint; + pAE.ReflectDamage = reflectsDamage; + pAE.HasOnFireDiscardables = hasOnFireDiscardables; + pAE.HasRestrictedArmorMultipliers = hasRestrictedArmorMultipliers; + pAE.HasCritModifiers = hasCritModifiers; + + if (forceDecloak && pThis->CloakState == CloakState::Cloaked) + pThis->Uncloak(true); + + if (wasTint || hasTint) + this->UpdateTintValues(); + + return false; +} + +// Recalculates tint values. +void TechnoExt::ExtData::UpdateTintValues() +{ + // reset values + this->TintColorOwner = 0; + this->TintColorAllies = 0; + this->TintColorEnemies = 0; + this->TintIntensityOwner = 0; + this->TintIntensityAllies = 0; + this->TintIntensityEnemies = 0; + + auto const pTypeExt = this->TypeExtData; + const bool hasTechnoTint = pTypeExt->Tint_Color.isset() || pTypeExt->Tint_Intensity; + const bool hasShieldTint = this->Shield && this->Shield->IsActive() && this->Shield->GetType()->HasTint(); + + // bail out early if no custom tint is applied. + if (!hasTechnoTint && !this->AE.HasTint && !hasShieldTint) + return; + + auto calculateTint = [this](const int color, const int intensity, const AffectedHouse affectedHouse) + { + if ((affectedHouse & AffectedHouse::Owner) != AffectedHouse::None) + { + this->TintColorOwner |= color; + this->TintIntensityOwner += intensity; + } + + if ((affectedHouse & AffectedHouse::Allies) != AffectedHouse::None) + { + this->TintColorAllies |= color; + this->TintIntensityAllies += intensity; + } + + if ((affectedHouse & AffectedHouse::Enemies) != AffectedHouse::None) + { + this->TintColorEnemies |= color; + this->TintIntensityEnemies += intensity; + } + }; + + if (hasTechnoTint) + calculateTint(Drawing::RGB_To_Int(pTypeExt->Tint_Color), static_cast(pTypeExt->Tint_Intensity * 1000), pTypeExt->Tint_VisibleToHouses); + + if (this->AE.HasTint) + { + for (auto const& attachEffect : this->AttachedEffects) + { + auto const type = attachEffect->GetType(); + + if (!attachEffect->IsActive() || !type->HasTint()) + continue; + + calculateTint(Drawing::RGB_To_Int(type->Tint_Color), static_cast(type->Tint_Intensity * 1000), type->Tint_VisibleToHouses); + } + } + + if (hasShieldTint) + { + auto const pShieldType = this->Shield->GetType(); + calculateTint(Drawing::RGB_To_Int(pShieldType->Tint_Color), static_cast(pShieldType->Tint_Intensity * 1000), pShieldType->Tint_VisibleToHouses); + } +} + +void TechnoExt::ExtData::UpdateLastTargetCrd() +{ + if (!this->TypeExtData->ExtraThreat_Enabled) + return; + + auto const pThis = this->OwnerObject(); + auto pTimer = &this->LastTargetCrdClearTimer; + + if (pThis->Target) + { + this->LastTargetCrd = pThis->Target->GetCoords(); + pTimer->Stop(); + } + else + { + if (!pTimer->IsTicking()) + pTimer->Start(45); + + if (pTimer->Completed()) + { + this->LastTargetCrd = CoordStruct::Empty; + pTimer->Stop(); + } + } +} diff --git a/VectorPort/src/New/Entity/AttachEffectClass.cpp b/VectorPort/src/New/Entity/AttachEffectClass.cpp new file mode 100644 index 0000000000..7fbfcc8c19 --- /dev/null +++ b/VectorPort/src/New/Entity/AttachEffectClass.cpp @@ -0,0 +1,1165 @@ +#include "AttachEffectClass.h" + +#include +#include +#include + +std::vector AttachEffectClass::Array; + +AttachEffectClass::AttachEffectClass() + : Type { nullptr }, Techno { nullptr }, InvokerHouse { nullptr }, Invoker { nullptr }, + Source { nullptr }, DurationOverride { 0 }, Delay { 0 }, InitialDelay { 0 }, RecreationDelay { -1 } + , Duration { 0 } + , CurrentDelay { 0 } + , NeedsDurationRefresh { false } + , HasCumulativeAnim { false } + , ShouldBeDiscarded { false } + , NeedsRecalculateStat { false } + , LastDiscardCheckFrame { -1 } + , LastDiscardCheckValue { false } + , Vector { } +{ + this->HasInitialized = false; + AttachEffectClass::Array.emplace_back(this); +} + +AttachEffectClass::AttachEffectClass(AttachEffectTypeClass* pType, TechnoClass* pTechno, HouseClass* pInvokerHouse, + TechnoClass* pInvoker, AbstractClass* pSource, int durationOverride, int delay, int initialDelay, int recreationDelay) + : Type { pType }, Techno { pTechno }, InvokerHouse { pInvokerHouse }, Invoker { pInvoker }, Source { pSource }, + DurationOverride { durationOverride }, Delay { delay }, InitialDelay { initialDelay }, RecreationDelay { recreationDelay } + , Duration { 0 } + , CurrentDelay { 0 } + , Animation { nullptr } + , IsAnimHidden { false } + , IsInTunnel { false } + , IsUnderTemporal { false } + , IsOnline { true } + , IsCloaked { false } + , LastActiveStat { true } + , LaserTrail { nullptr } + , NeedsDurationRefresh { false } + , HasCumulativeAnim { false } + , ShouldBeDiscarded { false } + , NeedsRecalculateStat { false } + , LastDiscardCheckFrame { -1 } + , LastDiscardCheckValue { false } +{ + this->HasInitialized = false; + + if (this->InitialDelay <= 0) + { + this->HasInitialized = true; + AttachEffectTypeClass::HandleEvent(pTechno); + } + + int& duration = this->Duration; + + duration = this->DurationOverride != 0 ? this->DurationOverride : pType->Duration; + + if (pType->Duration_ApplyFirepowerMult && duration > 0 && pInvoker) + duration = Math::max(static_cast(duration * TechnoExt::GetCurrentFirepowerMultiplier(pInvoker)), 0); + + const auto pTechnoExt = TechnoExt::ExtMap.Find(pTechno); + + if (pType->Duration_ApplyArmorMultOnTarget && duration > 0) // count its own ArmorMultiplier as well + { + const double armorMultiplier = TechnoExt::GetCurrentArmorMultiplier(pTechno, pTechnoExt->TypeExtData->OwnerObject()) * pType->ArmorMultiplier; + duration = Math::max(static_cast(duration / armorMultiplier), 0); + } + + const int laserTrailIdx = pType->LaserTrail_Type; + + if (laserTrailIdx != -1) + { + this->LaserTrail = pTechnoExt->LaserTrails.emplace_back(std::make_unique(LaserTrailTypeClass::Array[laserTrailIdx].get(), pTechno->Owner)).get(); + this->LaserTrail->Intrinsic = false; + } + + if (pInvoker) + TechnoExt::ExtMap.Find(pInvoker)->AttachedEffectInvokerCount++; + + AttachEffectClass::Array.emplace_back(this); +} + +AttachEffectClass::~AttachEffectClass() +{ + if (const auto& pTrail = this->LaserTrail) + { + const auto pTechnoExt = TechnoExt::ExtMap.Find(this->Techno); + const auto it = std::find_if(pTechnoExt->LaserTrails.cbegin(), pTechnoExt->LaserTrails.cend(), [pTrail](std::unique_ptr const& item) { return item.get() == pTrail; }); + + if (it != pTechnoExt->LaserTrails.cend()) + pTechnoExt->LaserTrails.erase(it); + + this->LaserTrail = nullptr; + } + + auto it = std::find(AttachEffectClass::Array.begin(), AttachEffectClass::Array.end(), this); + + if (it != AttachEffectClass::Array.end()) + AttachEffectClass::Array.erase(it); + + this->KillAnim(); + + if (this->Invoker) + TechnoExt::ExtMap.Find(this->Invoker)->AttachedEffectInvokerCount--; +} + +void AttachEffectClass::PointerGotInvalid(void* ptr, bool removed) +{ + if (!removed) // TODO: might be risky, needs further investigation + return; + + auto const abs = static_cast(ptr); + + if (auto const pAnim = abstract_cast(abs)) + { + if (auto const pAnimExt = AnimExt::ExtMap.Find(pAnim)) + { + if (pAnimExt->IsAttachedEffectAnim) + { + for (auto const pEffect : AttachEffectClass::Array) + { + if (pAnim == pEffect->Animation) + { + pEffect->Animation = nullptr; + break; // one anim must be used by less than one AE + } + } + } + } + } + else if ((abs->AbstractFlags & AbstractFlags::Techno) != AbstractFlags::None) + { + auto const pTechno = abstract_cast(abs); + + if (int count = TechnoExt::ExtMap.Find(pTechno)->AttachedEffectInvokerCount) + { + for (auto const pEffect : AttachEffectClass::Array) + { + if (pTechno == pEffect->Invoker) + { + AnnounceInvalidPointer(pEffect->Invoker, ptr); + count--; + + if (count <= 0) + break; + } + } + } + } +} + +// ============================= +// actual logic + +void AttachEffectClass::AI() +{ + auto const pTechno = this->Techno; + + if (!pTechno || pTechno->InLimbo || pTechno->IsImmobilized || pTechno->Transporter) + return; + + if (this->InitialDelay > 0) + { + this->InitialDelay--; + return; + } + + auto const pType = this->Type; + auto const pExt = TechnoExt::ExtMap.Find(pTechno); + + if (!this->HasInitialized && this->InitialDelay == 0) + { + this->HasInitialized = true; + + if (pType->ROFMultiplier > 0.0 && pType->ROFMultiplier_ApplyOnCurrentTimer) + { + const double ROFModifier = pType->ROFMultiplier; + pTechno->RearmTimer.Start(static_cast(pTechno->RearmTimer.GetTimeLeft() * ROFModifier)); + + if (!pExt->ChargeTurretTimer.HasStarted() && pExt->LastRearmWasFullDelay) + pTechno->ChargeTurretDelay = static_cast(pTechno->ChargeTurretDelay * ROFModifier); + } + + if (pType->HasTint()) + { + pTechno->MarkForRedraw(); + pExt->UpdateTintValues(); + } + + if (pExt->RecalculateStatMultipliers(this) && pTechno->CloakState == CloakState::Cloaked) + pTechno->Uncloak(true); + + AttachEffectTypeClass::HandleEvent(pTechno); + } + + if (this->CurrentDelay > 0) + { + if (!this->ShouldBeDiscardedNow()) + { + this->CurrentDelay--; + + if (this->CurrentDelay == 0) + this->NeedsDurationRefresh = true; + } + + return; + } + + if (this->NeedsDurationRefresh) + { + if (!this->ShouldBeDiscardedNow()) + { + this->RefreshDuration(); + + if (pExt->RecalculateStatMultipliers(this) && pTechno->CloakState == CloakState::Cloaked) + pTechno->Uncloak(true); + + this->NeedsDurationRefresh = false; + AttachEffectTypeClass::HandleEvent(pTechno); + } + + return; + } + + if (this->Duration > 0) + this->Duration--; + + if (this->Duration == 0) + { + const int delay = this->Delay; + + if (!this->IsSelfOwned() || delay < 0) + return; + + this->CurrentDelay = delay; + + if (delay > 0) + { + this->KillAnim(); + this->NeedsRecalculateStat = true; + } + else if (!this->ShouldBeDiscardedNow()) + { + this->RefreshDuration(); + } + else + { + this->NeedsDurationRefresh = true; + } + + return; + } + + if (this->IsUnderTemporal) + this->IsUnderTemporal = false; + + this->CloakCheck(); + this->OnlineCheck(); + this->AnimCheck(); + this->VectorAI(); +} + +void AttachEffectClass::AI_Temporal() +{ + if (!this->IsUnderTemporal) + { + this->IsUnderTemporal = true; + + this->CloakCheck(); + this->AnimCheck(); + + if (this->Animation) + { + switch (this->Type->Animation_TemporalAction) + { + case AttachedAnimFlag::Hides: + this->KillAnim(); + break; + case AttachedAnimFlag::Temporal: + this->Animation->UnderTemporal = true; + break; + + case AttachedAnimFlag::Paused: + this->Animation->Pause(); + break; + + case AttachedAnimFlag::PausedTemporal: + this->Animation->Pause(); + this->Animation->UnderTemporal = true; + break; + } + } + } +} + +void AttachEffectClass::AnimCheck() +{ + if (this->Type->Animation_HideIfAttachedWith.size() > 0) + { + auto const pTechnoExt = TechnoExt::ExtMap.Find(this->Techno); + + if (pTechnoExt->HasAttachedEffects(this->Type->Animation_HideIfAttachedWith, false, false, nullptr, nullptr, nullptr, nullptr)) + { + this->KillAnim(); + this->IsAnimHidden = true; + return; + } + } + + this->IsAnimHidden = false; + + if (!this->Animation && this->CanShowAnim()) + this->CreateAnim(); +} + +void AttachEffectClass::OnlineCheck() +{ + if (!this->Type->Powered) + return; + + auto const pTechno = this->Techno; + bool isActive = !(pTechno->Deactivated || pTechno->IsUnderEMP()); + + if (isActive && pTechno->WhatAmI() == AbstractType::Building) + { + auto const pBuilding = static_cast(pTechno); + isActive = pBuilding->IsPowerOnline(); + } + + this->IsOnline = isActive; + + if (isActive != this->LastActiveStat) + { + auto const pExt = TechnoExt::ExtMap.Find(pTechno); + + if (this->Type->HasTint()) + { + pTechno->MarkForRedraw(); + pExt->UpdateTintValues(); + } + + if (pExt->RecalculateStatMultipliers(this) && pTechno->CloakState == CloakState::Cloaked) + pTechno->Uncloak(true); + + this->LastActiveStat = isActive; + } + + if (!this->Animation) + return; + + if (!isActive) + { + switch (this->Type->Animation_OfflineAction) + { + case AttachedAnimFlag::Hides: + this->KillAnim(); + break; + + case AttachedAnimFlag::Temporal: + this->Animation->UnderTemporal = true; + break; + + case AttachedAnimFlag::Paused: + this->Animation->Pause(); + break; + + case AttachedAnimFlag::PausedTemporal: + this->Animation->Pause(); + this->Animation->UnderTemporal = true; + break; + } + } + else + { + this->Animation->UnderTemporal = false; + this->Animation->Unpause(); + } +} + +void AttachEffectClass::CloakCheck() +{ + const auto cloakState = this->Techno->CloakState; + this->IsCloaked = cloakState == CloakState::Cloaked || cloakState == CloakState::Cloaking; + + if (this->IsCloaked && this->Animation && AnimTypeExt::ExtMap.Find(this->Animation->Type)->DetachOnCloak) + this->KillAnim(); +} + +void AttachEffectClass::CreateAnim() +{ + auto const pType = this->Type; + + if (!pType) + return; + + AnimTypeClass* pAnimType = nullptr; + auto const pTechno = this->Techno; + + if (pType->Cumulative && pType->CumulativeAnimations.size() > 0) + { + if (!this->HasCumulativeAnim) + return; + + const int count = TechnoExt::ExtMap.Find(pTechno)->GetAttachedEffectCumulativeCount(pType); + pAnimType = pType->GetCumulativeAnimation(count); + } + else + { + pAnimType = pType->Animation; + } + + if (this->IsCloaked && (!pAnimType || AnimTypeExt::ExtMap.Find(pAnimType)->DetachOnCloak)) + return; + + if (!this->Animation && pAnimType) + { + auto const pAnim = GameCreate(pAnimType, pTechno->Location); + + pAnim->SetOwnerObject(pTechno); + pAnim->Owner = pType->Animation_UseInvokerAsOwner ? this->InvokerHouse : pTechno->Owner; + + auto const pAnimExt = AnimExt::ExtMap.Find(pAnim); + pAnimExt->IsAttachedEffectAnim = true; + + if (pType->Animation_UseInvokerAsOwner) + pAnimExt->SetInvoker(this->Invoker, this->InvokerHouse); + else + pAnimExt->SetInvoker(pTechno); + + pAnim->RemainingIterations = 0xFFu; + this->Animation = pAnim; + } +} + +void AttachEffectClass::KillAnim() +{ + if (this->Animation) + { + this->Animation->UnInit(); + this->Animation = nullptr; + } +} + +void AttachEffectClass::UpdateCumulativeAnim() +{ + if (!this->HasCumulativeAnim) + return; + + const auto pAnim = this->Animation; + + if (!pAnim) + return; + + const auto pType = this->Type; + const int count = TechnoExt::ExtMap.Find(this->Techno)->GetAttachedEffectCumulativeCount(pType); + + if (count < 1) + { + this->KillAnim(); + return; + } + + auto const pAnimType = pType->GetCumulativeAnimation(count); + + if (pAnim->Type != pAnimType) + AnimExt::ChangeAnimType(pAnim, pAnimType, false, pType->CumulativeAnimations_RestartOnChange); +} + +void AttachEffectClass::TransferCumulativeAnim(AttachEffectClass* pSource) +{ + if (!pSource || !pSource->Animation) + return; + + this->KillAnim(); + this->Animation = pSource->Animation; + this->HasCumulativeAnim = true; + pSource->Animation = nullptr; + pSource->HasCumulativeAnim = false; +} + +void AttachEffectClass::SetAnimationTunnelState(bool visible) +{ + if (!this->IsInTunnel && !visible) + this->KillAnim(); + + this->IsInTunnel = !visible; +} + +void AttachEffectClass::RefreshDuration(int durationOverride) +{ + int& duration = this->Duration; + auto const pType = this->Type; + + if (durationOverride) + duration = durationOverride; + else + duration = this->DurationOverride ? this->DurationOverride : pType->Duration; + + if (pType->Duration_ApplyFirepowerMult && duration > 0 && this->Invoker) + duration = Math::max(static_cast(duration * TechnoExt::GetCurrentFirepowerMultiplier(this->Invoker)), 0); + + if (pType->Duration_ApplyArmorMultOnTarget && duration > 0) // no need to count its own effect again + { + const auto pTechnoExt = TechnoExt::ExtMap.Find(this->Techno); + const double armorMultiplier = TechnoExt::GetCurrentArmorMultiplier(this->Techno, pTechnoExt->TypeExtData->OwnerObject()); + duration = Math::max(static_cast(duration / armorMultiplier), 0); + } + + if (pType->Animation_ResetOnReapply) + { + this->KillAnim(); + + if (this->CanShowAnim()) + this->CreateAnim(); + } +} + +bool AttachEffectClass::ResetIfRecreatable() +{ + if (!this->IsSelfOwned() || this->RecreationDelay < 0) + return false; + + this->KillAnim(); + this->Duration = 0; + this->CurrentDelay = this->RecreationDelay; + this->NeedsDurationRefresh = true; + + return true; +} + +bool AttachEffectClass::ShouldBeDiscardedNow() +{ + if (this->LastDiscardCheckFrame == Unsorted::CurrentFrame) + return this->LastDiscardCheckValue; + + this->LastDiscardCheckFrame = Unsorted::CurrentFrame; + + if (this->ShouldBeDiscarded) + { + this->LastDiscardCheckValue = true; + return true; + } + + auto const pType = this->Type; + auto const discardOn = pType->DiscardOn; + + if (discardOn == DiscardCondition::None) + { + this->LastDiscardCheckValue = false; + return false; + } + + auto const pTechno = this->Techno; + + if (auto const pFoot = abstract_cast(pTechno)) + { + const bool isMoving = this->Type->DiscardOn_MoveBasedOnDestination.Get(RulesExt::Global()->DiscardOn_MoveBasedOnDestination) + ? pFoot->Locomotor->Is_Moving() + : pFoot->Locomotor->Is_Really_Moving_Now(); + + if (isMoving) + { + if ((discardOn & DiscardCondition::Move) != DiscardCondition::None) + { + this->LastDiscardCheckValue = true; + return true; + } + } + else if ((discardOn & DiscardCondition::Stationary) != DiscardCondition::None) + { + this->LastDiscardCheckValue = true; + return true; + } + } + + if (pTechno->DrainingMe && (discardOn & DiscardCondition::Drain) != DiscardCondition::None) + { + this->LastDiscardCheckValue = true; + return true; + } + + if (pTechno->Target) + { + const bool inRange = (discardOn & DiscardCondition::InRange) != DiscardCondition::None; + const bool outOfRange = (discardOn & DiscardCondition::OutOfRange) != DiscardCondition::None; + + if (inRange || outOfRange) + { + int distance = -1; + + if (pType->DiscardOn_RangeOverride.isset()) + { + distance = pType->DiscardOn_RangeOverride.Get(); + } + else + { + const int weaponIndex = pTechno->SelectWeapon(pTechno->Target); + auto const pWeapon = pTechno->GetWeapon(weaponIndex)->WeaponType; + + if (pWeapon) + distance = WeaponTypeExt::GetRangeWithModifiers(pWeapon, pTechno); + } + + const int distanceFromTgt = pTechno->DistanceFrom(pTechno->Target); + + if ((inRange && distanceFromTgt <= distance) || (outOfRange && distanceFromTgt >= distance)) + { + this->LastDiscardCheckValue = true; + return true; + } + } + } + + this->LastDiscardCheckValue = false; + return false; +} + +#pragma region StaticFunctions_AttachDetachTransfer + +/// +/// Creates and attaches AttachEffects of given types to a techno. +/// +/// Target techno. +/// House that invoked the attachment. +/// Techno that invoked the attachment. +/// Source object for the attachment e.g a Warhead or Techno. +/// AttachEffect attach info. +/// Number of AttachEffect instances created and attached. +int AttachEffectClass::Attach(TechnoClass* pTarget, HouseClass* pInvokerHouse, TechnoClass* pInvoker, AbstractClass* pSource, AEAttachInfoTypeClass const& attachEffectInfo) +{ + auto const& types = attachEffectInfo.AttachTypes; + + if (types.size() < 1 || !pTarget) + return false; + + auto const pTargetExt = TechnoExt::ExtMap.Find(pTarget); + auto const pTargetType = pTargetExt->TypeExtData->OwnerObject(); + int attachedCount = 0; + bool markForRedraw = false; + bool decloak = false; + double ROFModifier = 1.0; + const bool selfOwned = pTarget == pSource; + + for (size_t i = 0; i < types.size(); i++) + { + auto const pType = types[i]; + auto const params = attachEffectInfo.GetAttachParams(i, selfOwned); + + if (auto const pAE = AttachEffectClass::CreateAndAttach(pType, pTarget, pTargetType, pTargetExt->AttachedEffects, pInvokerHouse, pInvoker, pSource, params)) + { + attachedCount++; + + if (params.InitialDelay <= 0) + { + if (pTargetExt->RecalculateStatMultipliers(pAE)) + decloak = true; + + if (pType->ROFMultiplier > 0.0 && pType->ROFMultiplier_ApplyOnCurrentTimer) + ROFModifier *= pType->ROFMultiplier; + + if (pType->HasTint()) + markForRedraw = true; + + if (pType->Cumulative && pType->CumulativeAnimations.size() > 0) + pTargetExt->UpdateCumulativeAttachEffects(pType); + } + } + } + + if (ROFModifier != 1.0) + { + pTarget->RearmTimer.Start(static_cast(pTarget->RearmTimer.GetTimeLeft() * ROFModifier)); + + if (!pTargetExt->ChargeTurretTimer.HasStarted() && pTargetExt->LastRearmWasFullDelay) + pTarget->ChargeTurretDelay = static_cast(pTarget->ChargeTurretDelay * ROFModifier); + } + + if (attachedCount > 0) + { + if (markForRedraw) + { + pTarget->MarkForRedraw(); + pTargetExt->UpdateTintValues(); + } + + if (decloak && pTarget->CloakState == CloakState::Cloaked) + pTarget->Uncloak(true); + } + + return attachedCount; +} + +/// +/// Creates and attaches a single AttachEffect instance of specified type on techno. +/// +/// AttachEffect type. +/// Target techno. +/// Target's AttachEffect vector +/// House that invoked the attachment. +/// Techno that invoked the attachment. +/// Source object for the attachment e.g a Warhead or Techno. +/// Attachment parameters. +/// Whether cumulative AE needs to be processed. +/// The created and attached AttachEffect if successful, nullptr if not. +AttachEffectClass* AttachEffectClass::CreateAndAttach(AttachEffectTypeClass* pType, TechnoClass* pTarget, TechnoTypeClass* pTargetType, std::vector>& targetAEs, + HouseClass* pInvokerHouse, TechnoClass* pInvoker, AbstractClass* pSource, AEAttachParams const& attachParams, bool checkCumulative) +{ + if (!pType) + return nullptr; + + if (pTarget->IsIronCurtained()) + { + const bool penetrates = pTarget->ForceShielded ? pType->PenetratesForceShield.Get(pType->PenetratesIronCurtain) : pType->PenetratesIronCurtain; + + if (!penetrates) + return nullptr; + } + + if (!EnumFunctions::IsTechnoEligible(pTarget, pType->AffectsTarget, true)) + return nullptr; + + if ((!pType->AffectTypes.empty() && !pType->AffectTypes.Contains(pTargetType)) || pType->IgnoreTypes.Contains(pTargetType)) + return nullptr; + + int currentTypeCount = 0; + int currentSourceCount = 0; + const bool cumulative = pType->Cumulative && checkCumulative; + AttachEffectClass* match = nullptr; + std::vector cumulativeMatches; + cumulativeMatches.reserve(targetAEs.size()); + + for (auto const& aePtr : targetAEs) + { + auto const attachEffect = aePtr.get(); + + if (attachEffect->GetType() == pType) + { + currentTypeCount++; + + if (!cumulative) + { + attachEffect->RefreshDuration(attachParams.DurationOverride); + AttachEffectTypeClass::HandleEvent(pTarget); + return nullptr; + } + else + { + if (attachEffect->IsFromSource(pInvoker, pSource)) + currentSourceCount++; + + if (!attachParams.CumulativeRefreshSameSourceOnly || attachEffect->IsFromSource(pInvoker, pSource)) + { + cumulativeMatches.push_back(attachEffect); + + if (!match || attachEffect->Duration < match->Duration) + match = attachEffect; + } + } + } + } + + if (cumulative) + { + if ((pType->Cumulative_MaxCount >= 0 && currentTypeCount >= pType->Cumulative_MaxCount) + || (attachParams.CumulativeSourceMaxCount >= 0 && currentSourceCount >= attachParams.CumulativeSourceMaxCount)) + { + if (attachParams.CumulativeRefreshAll) + { + for (auto const& ae : cumulativeMatches) + { + ae->RefreshDuration(attachParams.DurationOverride); + } + } + else if (match) + { + match->RefreshDuration(attachParams.DurationOverride); + } + + AttachEffectTypeClass::HandleEvent(pTarget); + return nullptr; + } + else if (attachParams.CumulativeRefreshAll && attachParams.CumulativeRefreshAll_OnAttach) + { + for (auto const& ae : cumulativeMatches) + { + ae->RefreshDuration(attachParams.DurationOverride); + } + } + } + + targetAEs.emplace_back(std::make_unique(pType, pTarget, pInvokerHouse, pInvoker, pSource, attachParams.DurationOverride, attachParams.Delay, attachParams.InitialDelay, attachParams.RecreationDelay)); + auto const pAE = targetAEs.back().get(); + + if (!currentTypeCount && cumulative && pType->CumulativeAnimations.size() > 0) + pAE->HasCumulativeAnim = true; + + return pAE; +} + +/// +/// Remove all AttachEffects matching given types from techno. +/// +/// Target techno. +/// AttachEffect attach info. +/// Number of AttachEffect instances removed. +int AttachEffectClass::Detach(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo) +{ + if (attachEffectInfo.RemoveTypes.size() < 1 || !pTarget) + return 0; + + return DetachTypes(pTarget, attachEffectInfo, attachEffectInfo.RemoveTypes); +} + +/// +/// Remove all AttachEffects matching given groups from techno. +/// +/// Target techno. +/// AttachEffect attach info. +/// Number of AttachEffect instances removed. +int AttachEffectClass::DetachByGroups(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo) +{ + auto const& groups = attachEffectInfo.RemoveGroups; + + if (groups.size() < 1 || !pTarget) + return 0; + + auto const pTargetExt = TechnoExt::ExtMap.Find(pTarget); + std::vector types; + types.reserve(pTargetExt->AttachedEffects.size()); + + for (auto const& attachEffect : pTargetExt->AttachedEffects) + { + auto const pType = attachEffect->Type; + + if (pType->HasGroups(groups, false)) + types.push_back(pType); + } + + return DetachTypes(pTarget, attachEffectInfo, types); +} + +/// +/// Remove all AttachEffects matching given types from techno. +/// +/// Target techno. +/// AttachEffect attach info. +/// AttachEffect types. +/// Number of AttachEffect instances removed. +int AttachEffectClass::DetachTypes(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo, std::vector const& types) +{ + int detachedCount = 0; + bool markForRedraw = false; + auto const& minCounts = attachEffectInfo.CumulativeRemoveMinCounts; + auto const& maxCounts = attachEffectInfo.CumulativeRemoveMaxCounts; + size_t index = 0; + const size_t minSize = minCounts.size(); + const size_t maxSize = maxCounts.size(); + + for (auto const pType : types) + { + const int minCount = minSize > 0 ? (index < minSize ? minCounts.at(index) : minCounts.at(minSize - 1)) : -1; + const int maxCount = maxSize > 0 ? (index < maxSize ? maxCounts.at(index) : maxCounts.at(maxSize - 1)) : -1; + + const int count = AttachEffectClass::RemoveAllOfType(pType, pTarget, minCount, maxCount); + + if (count && pType->HasTint()) + markForRedraw = true; + + detachedCount += count; + index++; + } + + if (detachedCount > 0) + { + TechnoExt::ExtMap.Find(pTarget)->RecalculateStatMultipliers(); + + if (markForRedraw) + pTarget->MarkForRedraw(); + } + + return detachedCount; +} + +/// +/// Remove all AttachEffects of given type from a techno. +/// +/// Type of AttachEffect to remove. +/// Target techno. +/// Minimum instance count needed for cumulative type to be removed. +/// Maximum instance count of cumulative type to be removed. +/// Number of AttachEffect instances removed. +int AttachEffectClass::RemoveAllOfType(AttachEffectTypeClass* pType, TechnoClass* pTarget, int minCount, int maxCount) +{ + if (!pType || !pTarget) + return 0; + + auto const pTargetExt = TechnoExt::ExtMap.Find(pTarget); + int detachedCount = 0; + int stackCount = -1; + + if (pType->Cumulative) + stackCount = pTargetExt->GetAttachedEffectCumulativeCount(pType); + + if (minCount > 0 && stackCount > -1 && pType->Cumulative && minCount > stackCount) + return 0; + + auto const targetAEs = &pTargetExt->AttachedEffects; + std::vector>::iterator it; + std::vector> expireWeapons; + + for (it = targetAEs->begin(); it != targetAEs->end(); ) + { + if (maxCount > 0 && detachedCount >= maxCount) + break; + + auto const attachEffect = it->get(); + + if (pType == attachEffect->Type) + { + detachedCount++; + + if (pType->ExpireWeapon && (pType->ExpireWeapon_TriggerOn & ExpireWeaponCondition::Remove) != ExpireWeaponCondition::None) + { + // can't be GetAttachedEffectCumulativeCount(pType) < 2, or inactive AE might make it stack more than once + if (!pType->Cumulative || !pType->ExpireWeapon_CumulativeOnlyOnce || stackCount == 1) + { + if (pType->ExpireWeapon_UseInvokerAsOwner) + { + if (auto const pInvoker = attachEffect->Invoker) + expireWeapons.push_back(std::make_pair(pType->ExpireWeapon, pInvoker)); + } + else + { + expireWeapons.push_back(std::make_pair(pType->ExpireWeapon, pTarget)); + } + } + } + + if (pType->Cumulative && pType->CumulativeAnimations.size() > 0) + pTargetExt->UpdateCumulativeAttachEffects(pType, attachEffect); + + if (attachEffect->ResetIfRecreatable()) + { + ++it; + continue; + } + + it = targetAEs->erase(it); + + if (!pType->Cumulative) + break; + + stackCount--; + } + else + { + ++it; + } + } + + auto const coords = pTarget->GetCoords(); + + for (auto const& pair : expireWeapons) + { + auto const pInvoker = pair.second; + WeaponTypeExt::DetonateAt(pair.first, coords, pInvoker, pInvoker->Owner, pTarget); + } + + return detachedCount; +} + +/// +/// Transfer AttachEffects from one techno to another. +/// +/// Source techno. +/// Target techno. +void AttachEffectClass::TransferAttachedEffects(TechnoClass* pSource, TechnoClass* pTarget) +{ + bool markForRedraw = false; + int transferCount = 0; + const auto pSourceExt = TechnoExt::ExtMap.Find(pSource); + const auto pTargetExt = TechnoExt::ExtMap.Find(pTarget); + const auto pTargetType = pTarget->GetTechnoType(); + std::vector>::iterator it; + + for (it = pSourceExt->AttachedEffects.begin(); it != pSourceExt->AttachedEffects.end(); ) + { + auto const attachEffect = it->get(); + + if (attachEffect->IsSelfOwned()) + { + ++it; + continue; + } + + auto const type = attachEffect->GetType(); + const bool isValid = EnumFunctions::IsTechnoEligible(pTarget, type->AffectsTarget, true) + && (type->AffectTypes.empty() || type->AffectTypes.Contains(pTargetType)) && !type->IgnoreTypes.Contains(pTargetType); + + if (!isValid) + { + it = pSourceExt->AttachedEffects.erase(it); + continue; + } + + int currentTypeCount = 0; + const bool cumulative = type->Cumulative; + AttachEffectClass* match = nullptr; + + for (auto const& aePtr : pTargetExt->AttachedEffects) + { + auto const targetAttachEffect = aePtr.get(); + + if (targetAttachEffect->GetType() == type) + { + currentTypeCount++; + + if (!cumulative) + { + match = targetAttachEffect; + break; + } + else if (targetAttachEffect->IsFromSource(attachEffect->Invoker, attachEffect->Source)) + { + if (!match || targetAttachEffect->Duration < match->Duration) + match = targetAttachEffect; + } + } + } + + if (match) + { + if (!cumulative || (type->Cumulative_MaxCount >= 0 && currentTypeCount >= type->Cumulative_MaxCount)) + match->Duration = Math::max(match->Duration, attachEffect->Duration); + } + else + { + AEAttachParams info {}; + info.DurationOverride = attachEffect->DurationOverride; + + if (auto const pAE = AttachEffectClass::CreateAndAttach(type, pTarget, pTargetType, pTargetExt->AttachedEffects, attachEffect->InvokerHouse, attachEffect->Invoker, attachEffect->Source, info, false)) + pAE->Duration = attachEffect->Duration; + } + + if (type->HasTint()) + markForRedraw = true; + + transferCount++; + it = pSourceExt->AttachedEffects.erase(it); + } + + if (transferCount > 0) + { + pTargetExt->RecalculateStatMultipliers(); + + if (markForRedraw) + pTarget->MarkForRedraw(); + } +} + +#pragma endregion + +CoordStruct AttachEffectClass::GetFLHAbsoluteCoords(CoordStruct origin, CoordStruct flh, DirStruct facing) +{ + double rad = facing.GetRadian<32>(); + double cosR = std::cos(rad); + double sinR = std::sin(rad); + return { origin.X + static_cast(flh.X * cosR + flh.Y * sinR), + origin.Y + static_cast(flh.X * sinR - flh.Y * cosR), + origin.Z + flh.Z }; +} + +void AttachEffectClass::VectorAI() +{ + if (!this->Type->Vector_AffectTechno) + return; + VectorAI_Run(this->Techno, this->Type, this->Vector, this->Invoker, false); +} + +// ============================= +// load / save + +template +bool AttachEffectClass::Serialize(T& Stm) +{ + return Stm + .Process(this->Duration) + .Process(this->DurationOverride) + .Process(this->Delay) + .Process(this->CurrentDelay) + .Process(this->InitialDelay) + .Process(this->RecreationDelay) + .Process(this->Type) + .Process(this->Techno) + .Process(this->InvokerHouse) + .Process(this->Invoker) + .Process(this->Source) + .Process(this->Animation) + .Process(this->IsAnimHidden) + .Process(this->IsInTunnel) + .Process(this->IsUnderTemporal) + .Process(this->IsOnline) + .Process(this->IsCloaked) + .Process(this->HasInitialized) + .Process(this->NeedsDurationRefresh) + .Process(this->LastDiscardCheckFrame) + .Process(this->LastDiscardCheckValue) + .Process(this->HasCumulativeAnim) + .Process(this->ShouldBeDiscarded) + .Process(this->LastActiveStat) + .Process(this->LaserTrail) + .Process(this->NeedsRecalculateStat) + .Process(this->Vector.Initialized) + .Process(this->Vector.CurrentFrame) + .Process(this->Vector.DisabledTimer) + .Process(this->Vector.CurrentSpeed) + .Process(this->Vector.CurrentAngle) + .Process(this->Vector.CurrentCircleRadius) + .Process(this->Vector.CurrentCircleSpeed) + .Process(this->Vector.CurrentCircleAngle) + .Process(this->Vector.InitialOriginPos) + .Process(this->Vector.InitialLocation) + .Process(this->Vector.PrevCirclePos) + .Process(this->Vector.ArcHeight) + .Process(this->Vector.ArcRotation) + .Process(this->Vector.TargetOffset) + .Process(this->Vector.NormalRotF) + .Process(this->Vector.NormalRotL) + .Process(this->Vector.NormalRotH) + .Process(this->Vector.NormalStepF) + .Process(this->Vector.NormalStepL) + .Process(this->Vector.NormalStepH) + .Process(this->Vector.MovementFrames) + .Process(this->Vector.FacingRad) + .Process(this->Vector.TiltRad) + .Process(this->Vector.OriginOffset) + .Process(this->Vector.PrevCircleCenter) + .Process(this->Vector.OriginElapsed) + .Process(this->Vector.OriginSpeed) + .Process(this->Vector.OriginAngle) + .Process(this->Vector.OriginTargetOffset) + .Process(this->Vector.OriginCircleRadiusRuntime) + .Process(this->Vector.OriginCircleSpeedRuntime) + .Process(this->Vector.OriginCircleAngleRuntime) + .Process(this->Vector.OriginNormalRotFRuntime) + .Process(this->Vector.OriginNormalRotLRuntime) + .Process(this->Vector.OriginNormalRotHRuntime) + .Process(this->Vector.OriginNormalStepF) + .Process(this->Vector.OriginNormalStepL) + .Process(this->Vector.OriginNormalStepH) + .Process(this->Vector.OriginFacing) + .Process(this->Vector.OriginTilt) + .Success(); +} + +bool AttachEffectClass::Load(PhobosStreamReader& Stm, bool RegisterForChange) +{ + return Serialize(Stm); +} + +bool AttachEffectClass::Save(PhobosStreamWriter& Stm) const +{ + return const_cast(this)->Serialize(Stm); +} diff --git a/VectorPort/src/New/Entity/AttachEffectClass.h b/VectorPort/src/New/Entity/AttachEffectClass.h new file mode 100644 index 0000000000..50f55b8a84 --- /dev/null +++ b/VectorPort/src/New/Entity/AttachEffectClass.h @@ -0,0 +1,149 @@ +#pragma once + +#include +#include + +class LaserTrailClass; + +class AttachEffectClass +{ +public: + static std::vector Array; + + AttachEffectClass(); + + AttachEffectClass(AttachEffectTypeClass* pType, TechnoClass* pTechno, HouseClass* pInvokerHouse, TechnoClass* pInvoker, + AbstractClass* pSource, int durationOverride, int delay, int initialDelay, int recreationDelay); + + ~AttachEffectClass(); + + void AI(); + void AI_Temporal(); + void KillAnim(); + void CreateAnim(); + void UpdateCumulativeAnim(); + void TransferCumulativeAnim(AttachEffectClass* pSource); + void VectorAI(); + + bool CanShowAnim() const + { + return (this->IsOnline || this->Type->Animation_OfflineAction != AttachedAnimFlag::Hides) + && (!this->IsUnderTemporal || this->Type->Animation_TemporalAction != AttachedAnimFlag::Hides) + && !this->IsAnimHidden && !this->IsInTunnel; + } + + void SetAnimationTunnelState(bool visible); + AttachEffectTypeClass* GetType() const { return this->Type; } + int GetRemainingDuration() const { return this->Duration; } + void RefreshDuration(int durationOverride = 0); + bool ResetIfRecreatable(); + bool IsSelfOwned() const { return this->Source == this->Techno; } + bool HasExpired() const { return this->IsSelfOwned() && this->Delay >= 0 ? false : !this->Duration; } + bool ShouldBeDiscardedNow(); + bool IsFromSource(TechnoClass* pInvoker, AbstractClass* pSource) const { return pInvoker == this->Invoker && pSource == this->Source; } + TechnoClass* GetInvoker() const { return this->Invoker; } + bool IsActive() const { return this->IsOnline && this->IsActiveIgnorePowered(); } + + bool IsActiveIgnorePowered() const + { + if (this->IsSelfOwned()) + return this->InitialDelay <= 0 && this->CurrentDelay == 0 && this->HasInitialized && !this->NeedsDurationRefresh; + else + return this->Duration; + } + + static void PointerGotInvalid(void* ptr, bool removed); + bool Load(PhobosStreamReader& Stm, bool RegisterForChange); + bool Save(PhobosStreamWriter& Stm) const; + + static int Attach(TechnoClass* pTarget, HouseClass* pInvokerHouse, TechnoClass* pInvoker, AbstractClass* pSource, AEAttachInfoTypeClass const& attachEffectInfo); + static int Detach(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo); + static int DetachByGroups(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo); + static void TransferAttachedEffects(TechnoClass* pSource, TechnoClass* pTarget); + static CoordStruct GetFLHAbsoluteCoords(CoordStruct origin, CoordStruct flh, DirStruct facing); + + static AttachEffectClass* CreateAndAttach(AttachEffectTypeClass* pType, TechnoClass* pTarget, TechnoTypeClass* pTargetType, std::vector>& targetAEs, HouseClass* pInvokerHouse, TechnoClass* pInvoker, + AbstractClass* pSource, AEAttachParams const& attachInfo, bool checkCumulative = true); + +private: + void OnlineCheck(); + void CloakCheck(); + void AnimCheck(); + + static int DetachTypes(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo, std::vector const& types); + static int RemoveAllOfType(AttachEffectTypeClass* pType, TechnoClass* pTarget, int minCount, int maxCount); + + template + bool Serialize(T& Stm); + + int Duration; + int DurationOverride; + int Delay; + int CurrentDelay; + int InitialDelay; + int RecreationDelay; + AttachEffectTypeClass* Type; + TechnoClass* Techno; + +public: + HouseClass* InvokerHouse; + TechnoClass* Invoker; + AbstractClass* Source; + +private: + AnimClass* Animation; + bool IsAnimHidden; + bool IsInTunnel; + bool IsUnderTemporal; + bool IsOnline; + bool IsCloaked; + bool HasInitialized; + bool NeedsDurationRefresh; + int LastDiscardCheckFrame; + bool LastDiscardCheckValue; + bool LastActiveStat; + LaserTrailClass* LaserTrail; + +public: + bool HasCumulativeAnim; + bool ShouldBeDiscarded; + bool NeedsRecalculateStat; + + VectorState Vector; +}; + +// Container for TechnoClass-specific AttachEffect fields. +struct AttachEffectTechnoProperties +{ + double FirepowerMultiplier; + double ArmorMultiplier; + double SpeedMultiplier; + double ROFMultiplier; + bool Cloakable; + bool ForceDecloak; + bool DisableWeapons; + bool Unkillable; + bool HasRangeModifier; + bool HasTint; + bool ReflectDamage; + bool HasOnFireDiscardables; + bool HasRestrictedArmorMultipliers; + bool HasCritModifiers; + + AttachEffectTechnoProperties() : + FirepowerMultiplier { 1.0 } + , ArmorMultiplier { 1.0 } + , SpeedMultiplier { 1.0 } + , ROFMultiplier { 1.0 } + , Cloakable { false } + , ForceDecloak { false } + , DisableWeapons { false } + , Unkillable { false } + , HasRangeModifier { false } + , HasTint { false } + , ReflectDamage { false } + , HasOnFireDiscardables { false } + , HasRestrictedArmorMultipliers { false } + , HasCritModifiers { false } + { } +}; diff --git a/VectorPort/src/New/Entity/VectorState.cpp b/VectorPort/src/New/Entity/VectorState.cpp new file mode 100644 index 0000000000..c440167842 --- /dev/null +++ b/VectorPort/src/New/Entity/VectorState.cpp @@ -0,0 +1,752 @@ +#include "VectorState.h" +#include "AttachEffectClass.h" +#include +#include + +static double V_Random(double min, double max) +{ + if (min >= max) return min; + return min + (max - min) * static_cast(std::rand()) / RAND_MAX; +} + +static double V_Deg2Rad(double deg) { return deg * 3.14159265358979323846 / 180.0; } +static double V_Rad2Deg(double rad) { return rad * 180.0 / 3.14159265358979323846; } + +static DirStruct V_Radians2Dir(double rad) +{ + return DirStruct(static_cast(rad * 32768.0 / 3.14159265358979323846)); +} + +static DirStruct V_Point2Dir(CoordStruct from, CoordStruct to) +{ + double dx = to.X - from.X, dy = to.Y - from.Y; + return V_Radians2Dir(std::atan2(dy, dx)); +} + +static CoordStruct V_FLHAbsoluteOffset(CoordStruct flh, DirStruct facing) +{ + return AttachEffectClass::GetFLHAbsoluteCoords(CoordStruct::Empty, flh, facing); +} + +void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorState& s, ObjectClass* pInvoker, bool isBullet) +{ + if (!pObject) + return; + + if (s.DisabledTimer > 0) + { + s.DisabledTimer--; + return; + } + + bool skipFrame = pType->Vector_TimeStep > 1 && (s.CurrentFrame % pType->Vector_TimeStep != 0); + s.CurrentFrame++; + + if (skipFrame) + return; + + s.MovementFrames++; + s.NormalRotF += s.NormalStepF; + s.NormalRotL += s.NormalStepL; + s.NormalRotH += s.NormalStepH; + + auto GetPos = [&]() { return pObject->GetCoords(); }; + + if (!s.Initialized) + { + s.Initialized = true; + s.InitialOriginPos = GetPos(); + s.InitialLocation = GetPos(); + + if (isBullet) + { + auto const pB = static_cast(pObject); + double vx = pB->Velocity.X, vy = pB->Velocity.Y; + s.FacingRad = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + } + else + { + auto const pT = static_cast(pObject); + s.FacingRad = pT->PrimaryFacing.Current().GetRadian<32>(); + } + s.TiltRad = 0.0; + s.OriginFacing = s.FacingRad; + s.OriginTilt = 0.0; + + double speed = static_cast(pType->Vector_InitialSpeed >= 0 ? pType->Vector_InitialSpeed : 0); + if (pType->Vector_RandomSpeedMin != pType->Vector_RandomSpeedMax) + speed = V_Random(pType->Vector_RandomSpeedMin, pType->Vector_RandomSpeedMax); + s.CurrentSpeed = speed; + + s.ArcHeight = static_cast(pType->Vector_ArcHeight); + if (pType->Vector_ArcRandomHeightMin != pType->Vector_ArcRandomHeightMax) + s.ArcHeight = V_Random(pType->Vector_ArcRandomHeightMin, pType->Vector_ArcRandomHeightMax); + s.ArcRotation = pType->Vector_ArcRotation; + if (pType->Vector_ArcRandomRotationMin != pType->Vector_ArcRandomRotationMax) + s.ArcRotation = V_Random(pType->Vector_ArcRandomRotationMin, pType->Vector_ArcRandomRotationMax); + + if (pType->Vector_TargetFLH.isset()) + { + CoordStruct offset = pType->Vector_TargetFLH.Get(); + if (pType->Vector_TargetOffsetFMin != pType->Vector_TargetOffsetFMax) + offset.X = pType->Vector_TargetOffsetFMin + rand() % (pType->Vector_TargetOffsetFMax - pType->Vector_TargetOffsetFMin + 1); + if (pType->Vector_TargetOffsetLMin != pType->Vector_TargetOffsetLMax) + offset.Y = pType->Vector_TargetOffsetLMin + rand() % (pType->Vector_TargetOffsetLMax - pType->Vector_TargetOffsetLMin + 1); + if (pType->Vector_TargetOffsetHMin != pType->Vector_TargetOffsetHMax) + offset.Z = pType->Vector_TargetOffsetHMin + rand() % (pType->Vector_TargetOffsetHMax - pType->Vector_TargetOffsetHMin + 1); + s.TargetOffset = offset; + } + + if (pType->Vector_NormalVector.isset()) + { + s.NormalRotF = 0.0; s.NormalRotL = 0.0; s.NormalRotH = 0.0; + auto ns = [](double ps, double m1, double M1, double m2, double M2) { + if (ps != 0.0) return ps; + if (M2 > m2 && (rand() % 2)) return m2 + (rand() / (double)RAND_MAX) * (M2 - m2); + return M1 > m1 ? m1 + (rand() / (double)RAND_MAX) * (M1 - m1) : 0.0; + }; + s.NormalStepF = ns(pType->Vector_NormalFAnglePerStep, pType->Vector_NormalFAngleRMin, pType->Vector_NormalFAngleRMax, pType->Vector_NormalFAngleRMin2, pType->Vector_NormalFAngleRMax2); + s.NormalStepL = ns(pType->Vector_NormalLAnglePerStep, pType->Vector_NormalLAngleRMin, pType->Vector_NormalLAngleRMax, pType->Vector_NormalLAngleRMin2, pType->Vector_NormalLAngleRMax2); + s.NormalStepH = ns(pType->Vector_NormalHAnglePerStep, pType->Vector_NormalHAngleRMin, pType->Vector_NormalHAngleRMax, pType->Vector_NormalHAngleRMin2, pType->Vector_NormalHAngleRMax2); + } + + if (pType->Vector_OriginNormalVector.isset()) + { + auto ns = [](double ps, double m1, double M1, double m2, double M2) { + if (ps != 0.0) return ps; + if (M2 > m2 && (rand() % 2)) return m2 + (rand() / (double)RAND_MAX) * (M2 - m2); + return M1 > m1 ? m1 + (rand() / (double)RAND_MAX) * (M1 - m1) : 0.0; + }; + s.OriginNormalStepF = ns(pType->Vector_OriginNormalFAnglePerStep, pType->Vector_OriginNormalFAngleRMin, pType->Vector_OriginNormalFAngleRMax, pType->Vector_OriginNormalFAngleRMin2, pType->Vector_OriginNormalFAngleRMax2); + s.OriginNormalStepL = ns(pType->Vector_OriginNormalLAnglePerStep, pType->Vector_OriginNormalLAngleRMin, pType->Vector_OriginNormalLAngleRMax, pType->Vector_OriginNormalLAngleRMin2, pType->Vector_OriginNormalLAngleRMax2); + s.OriginNormalStepH = ns(pType->Vector_OriginNormalHAnglePerStep, pType->Vector_OriginNormalHAngleRMin, pType->Vector_OriginNormalHAngleRMax, pType->Vector_OriginNormalHAngleRMin2, pType->Vector_OriginNormalHAngleRMax2); + } + else + { + s.OriginNormalStepF = pType->Vector_OriginNormalFAnglePerStep; + s.OriginNormalStepL = pType->Vector_OriginNormalLAnglePerStep; + s.OriginNormalStepH = pType->Vector_OriginNormalHAnglePerStep; + } + } + + // === Dynamic facing === + double effectiveFacing = s.FacingRad + V_Deg2Rad(s.NormalRotH); + double effectiveTilt = s.TiltRad + V_Deg2Rad(s.NormalRotL); + DirStruct mainFacingDir = V_Radians2Dir(effectiveFacing); + + if (pType->Vector_OriginIsOnWorld) + { + mainFacingDir = DirStruct{}; + effectiveFacing = 0.0; + effectiveTilt = 0.0; + } + + bool hasNormal = pType->Vector_NormalVector.isset(); + if (!pType->Vector_OriginNoUpdate && !hasNormal && !pType->Vector_OriginIsOnWorld) + { + switch (pType->Vector_Origin) + { + case VectorOrigin::Source: + if (pInvoker) + { + mainFacingDir = V_Point2Dir(pInvoker->GetCoords(), GetPos()); + effectiveFacing = mainFacingDir.GetRadian<32>(); + if (pType->Vector_AllowedTilt) + { + double dx = GetPos().X - pInvoker->GetCoords().X; + double dy = GetPos().Y - pInvoker->GetCoords().Y; + double dz = GetPos().Z - pInvoker->GetCoords().Z; + double lenXY = std::sqrt(dx * dx + dy * dy); + effectiveTilt = (lenXY > 1e-6) ? std::atan2(dz, lenXY) : 0.0; + } + } + break; + case VectorOrigin::Target: + { + CoordStruct targetPos; + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) + targetPos = pB->Target->GetCoords(); + else + break; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) + targetPos = pT->Target->GetCoords(); + else + break; + } + mainFacingDir = V_Point2Dir(targetPos, GetPos()); + effectiveFacing = mainFacingDir.GetRadian<32>(); + if (pType->Vector_AllowedTilt) + { + double dx = GetPos().X - targetPos.X; + double dy = GetPos().Y - targetPos.Y; + double dz = GetPos().Z - targetPos.Z; + double lenXY = std::sqrt(dx * dx + dy * dy); + effectiveTilt = (lenXY > 1e-6) ? std::atan2(dz, lenXY) : 0.0; + } + break; + } + case VectorOrigin::Self: + if (isBullet) + { + double vx = static_cast(pObject)->Velocity.X; + double vy = static_cast(pObject)->Velocity.Y; + effectiveFacing = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + mainFacingDir = V_Radians2Dir(effectiveFacing); + } + else + { + auto const pT = static_cast(pObject); + mainFacingDir = pType->Vector_OriginIsOnBody + ? pT->PrimaryFacing.Current() + : pT->TurretFacing(); + effectiveFacing = mainFacingDir.GetRadian<32>(); + } + break; + case VectorOrigin::Launcher: + if (pInvoker) + { + auto const pLT = static_cast(pInvoker); + mainFacingDir = pType->Vector_OriginIsOnBody + ? pLT->PrimaryFacing.Current() + : pLT->TurretFacing(); + effectiveFacing = mainFacingDir.GetRadian<32>(); + } + break; + } + } + + // === Origin coordinate (per-frame tracking) === + CoordStruct originPos = GetPos(); + + switch (pType->Vector_Origin) + { + case VectorOrigin::Target: + if (pType->Vector_OriginNoUpdate) + originPos = s.InitialOriginPos; + else if (isBullet) + { + auto const pB = static_cast(pObject); + originPos = pB->Target ? pB->Target->GetCoords() : pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + originPos = pT->Target ? pT->Target->GetCoords() : originPos; + } + break; + case VectorOrigin::Launcher: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Source: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Self: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : GetPos(); + break; + } + + if (pType->Vector_OriginFLH.isset() && pType->Vector_Origin != VectorOrigin::Self) + originPos = AttachEffectClass::GetFLHAbsoluteCoords(originPos, pType->Vector_OriginFLH, mainFacingDir); + + CoordStruct currentPos = GetPos(); + + // ================================================================ + // Circle mode + // ================================================================ + bool hasCircle = pType->Vector_CircleRadius > 0 || pType->Vector_CircleSpeed != 0 || pType->Vector_CircleAnglePerStep > 0.0 + || (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + || (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin); + + if (hasCircle) + { + double calcRadius = static_cast(pType->Vector_CircleRadius); + if (calcRadius <= 0.0) + { + double tdx = currentPos.X - originPos.X; + double tdy = currentPos.Y - originPos.Y; + calcRadius = std::sqrt(tdx * tdx + tdy * tdy); + } + + if (s.MovementFrames == 1) + { + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleSpeed); + s.CurrentCircleRadius = calcRadius; + if (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + s.CurrentCircleRadius = static_cast(rand() % (pType->Vector_CircleRandomRadiusMax - pType->Vector_CircleRandomRadiusMin + 1) + pType->Vector_CircleRandomRadiusMin); + } + s.CurrentCircleSpeed += pType->Vector_CircleSpeedAcceleration; + if (pType->Vector_CircleMaxSpeed != 0 && s.CurrentCircleSpeed > pType->Vector_CircleMaxSpeed) + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleMaxSpeed); + if (pType->Vector_CircleMinSpeed != 0 && s.CurrentCircleSpeed < pType->Vector_CircleMinSpeed) + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleMinSpeed); + + if (s.MovementFrames == 1) + { + s.CurrentCircleAngle = pType->Vector_CircleAnglePerStep; + if (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin) + s.CurrentCircleAngle = V_Random(pType->Vector_CircleRandomAngleMin, pType->Vector_CircleRandomAngleMax); + } + s.CurrentCircleAngle += pType->Vector_CircleAngleAcceleration; + if (pType->Vector_CircleMaxAngle != 0 && s.CurrentCircleAngle > pType->Vector_CircleMaxAngle) + s.CurrentCircleAngle = pType->Vector_CircleMaxAngle; + if (pType->Vector_CircleMinAngle != 0 && s.CurrentCircleAngle < pType->Vector_CircleMinAngle) + s.CurrentCircleAngle = pType->Vector_CircleMinAngle; + + double speed = s.CurrentCircleSpeed; + double angleStep = s.CurrentCircleAngle; + if (speed <= 0.0 && angleStep > 0.0) + speed = calcRadius * V_Deg2Rad(angleStep); + else if (angleStep <= 0.0 && speed > 0.0) + angleStep = V_Rad2Deg(speed / calcRadius); + + CoordStruct circleCenter = originPos; + if (pType->Vector_CircleOrigin.isset()) + { + if (pType->Vector_AllowOriginTilt) + circleCenter = AttachEffectClass::GetFLHAbsoluteCoords(originPos, pType->Vector_CircleOrigin, mainFacingDir); + else + circleCenter = { originPos.X + pType->Vector_CircleOrigin.Get().X, originPos.Y + pType->Vector_CircleOrigin.Get().Y, originPos.Z + pType->Vector_CircleOrigin.Get().Z }; + } + + bool hasOriginSub = pType->Vector_OriginMoveTo.isset() + || pType->Vector_OriginTargetFLH.isset() + || pType->Vector_OriginCircleRadius >= 0 || pType->Vector_OriginCircleSpeed != 0 || pType->Vector_OriginCircleAnglePerStep != 0; + + if (hasOriginSub) + { + CoordStruct baseCenter = originPos; + + if (pType->Vector_OriginOrigin != VectorOrigin::Self) + { + switch (pType->Vector_OriginOrigin) + { + case VectorOrigin::Launcher: + if (pInvoker) baseCenter = pInvoker->GetCoords(); + break; + case VectorOrigin::Target: + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) baseCenter = pB->Target->GetCoords(); + else if (pB->Owner && pB->Owner->Target) baseCenter = pB->Owner->Target->GetCoords(); + else baseCenter = pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) baseCenter = pT->Target->GetCoords(); + else + { + auto const pFoot = abstract_cast(pT); + if (pFoot && pFoot->Destination) baseCenter = pFoot->Destination->GetCoords(); + } + } + break; + case VectorOrigin::Source: + if (pInvoker) baseCenter = pInvoker->GetCoords(); + break; + } + } + else if (pType->Vector_OriginOriginFLH.isset()) + { + baseCenter.X += pType->Vector_OriginOriginFLH.Get().X; + baseCenter.Y += pType->Vector_OriginOriginFLH.Get().Y; + baseCenter.Z += pType->Vector_OriginOriginFLH.Get().Z; + } + + if (s.MovementFrames == 1) + { + s.OriginOffset = { circleCenter.X - baseCenter.X, circleCenter.Y - baseCenter.Y, circleCenter.Z - baseCenter.Z }; + s.OriginCircleRadiusRuntime = static_cast(pType->Vector_OriginCircleRadius); + s.OriginCircleSpeedRuntime = static_cast(pType->Vector_OriginCircleSpeed); + s.OriginCircleAngleRuntime = 0.0; + s.OriginTargetOffset = CoordStruct::Empty; + + if (pType->Vector_OriginNormalVector.isset()) + { + CoordStruct nv = pType->Vector_OriginNormalVector.Get(); + double len = std::sqrt(static_cast(nv.X * nv.X + nv.Y * nv.Y)); + s.OriginFacing = len > 1e-6 ? std::atan2(static_cast(nv.Y), static_cast(nv.X)) : 0; + s.OriginTilt = len > 1e-6 ? std::atan2(static_cast(nv.Z), len) : (nv.Z > 0 ? 3.14159265358979323846 / 2.0 : -3.14159265358979323846 / 2.0); + } + } + + s.OriginNormalRotFRuntime += s.OriginNormalStepF; + s.OriginNormalRotLRuntime += s.OriginNormalStepL; + s.OriginNormalRotHRuntime += s.OriginNormalStepH; + + double oFacing = s.OriginFacing + V_Deg2Rad(s.OriginNormalRotHRuntime); + double oTilt = s.OriginTilt + V_Deg2Rad(s.OriginNormalRotLRuntime); + + CoordStruct originCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + CoordStruct disp{ 0, 0, 0 }; + + if (pType->Vector_OriginMoveTo.isset()) + { + s.OriginAngle += pType->Vector_OriginCircleAnglePerStep; + CoordStruct mto = pType->Vector_OriginMoveTo.Get(); + if (pType->Vector_OriginGrowRate.isset()) + { + mto.X += pType->Vector_OriginGrowRate.Get().X * s.OriginElapsed; + mto.Y += pType->Vector_OriginGrowRate.Get().Y * s.OriginElapsed; + mto.Z += pType->Vector_OriginGrowRate.Get().Z * s.OriginElapsed; + } + disp = V_FLHAbsoluteOffset(mto, V_Radians2Dir(oFacing + V_Deg2Rad(s.OriginAngle))); + } + else if (pType->Vector_OriginTargetFLH.isset()) + { + if (s.OriginElapsed == 0) + s.OriginSpeed = pType->Vector_OriginInitialSpeed >= 0 ? pType->Vector_OriginInitialSpeed : 40.0; + + CoordStruct targetWorld = AttachEffectClass::GetFLHAbsoluteCoords(baseCenter, pType->Vector_OriginTargetFLH.Get() + s.OriginTargetOffset, V_Radians2Dir(oFacing)); + int dx = targetWorld.X - originCenter.X, dy = targetWorld.Y - originCenter.Y, dz = targetWorld.Z - originCenter.Z; + double dist = std::sqrt(static_cast(dx * dx + dy * dy + dz * dz)); + if (dist >= 1.0) + { + double sv = s.OriginSpeed / dist; + disp = { static_cast(dx * sv), static_cast(dy * sv), static_cast(dz * sv) }; + } + } + else + { + s.OriginCircleRadiusRuntime += pType->Vector_OriginCircleRadiusGrow; + double tr = s.OriginCircleRadiusRuntime; + if (pType->Vector_OriginCircleMaxRadius > 0 && tr > pType->Vector_OriginCircleMaxRadius) tr = pType->Vector_OriginCircleMaxRadius; + if (pType->Vector_OriginCircleMinRadius > 0 && tr < pType->Vector_OriginCircleMinRadius) tr = pType->Vector_OriginCircleMinRadius; + double stepO = pType->Vector_OriginCircleAnglePerStep; + if (pType->Vector_OriginCircleSpeed != 0 && tr > 0) + stepO = V_Rad2Deg(pType->Vector_OriginCircleSpeed / tr); + s.OriginCircleAngleRuntime += stepO; + double r = V_Deg2Rad(stepO); + double ca = std::cos(r), sa = std::sin(r); + double dxO = static_cast(s.OriginOffset.X); + double dyO = static_cast(s.OriginOffset.Y); + double dzO = static_cast(s.OriginOffset.Z); + double cf = std::cos(oFacing), sf = std::sin(oFacing), ct = std::cos(oTilt), st = std::sin(oTilt); + double dL = dxO * (-sf) + dyO * cf; + double dH = dxO * (-cf * st) + dyO * (-sf * st) + dzO * ct; + double cd = std::sqrt(dL * dL + dH * dH); + if (cd < 1.0 && tr > 0) { dL = tr; dH = 0; cd = tr; } + else if (cd < 1.0) cd = 1.0; + double rL = (dL / cd * tr * ca) - (dH / cd * tr * sa); + double rH = (dL / cd * tr * sa) + (dH / cd * tr * ca); + CoordStruct newOffset; + newOffset.X = static_cast(rL * (-sf) + rH * (-cf * st)); + newOffset.Y = static_cast(rL * cf + rH * (-sf * st)); + newOffset.Z = static_cast(rH * ct); + disp.X = newOffset.X - s.OriginOffset.X; + disp.Y = newOffset.Y - s.OriginOffset.Y; + disp.Z = newOffset.Z - s.OriginOffset.Z; + } + + s.OriginOffset.X += disp.X; s.OriginOffset.Y += disp.Y; s.OriginOffset.Z += disp.Z; + circleCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + s.OriginElapsed++; + } + + CoordStruct centerDelta{ 0, 0, 0 }; + bool useCenterTracking = false; + if (s.PrevCircleCenter.X || s.PrevCircleCenter.Y || s.PrevCircleCenter.Z) + { + centerDelta.X = circleCenter.X - s.PrevCircleCenter.X; + centerDelta.Y = circleCenter.Y - s.PrevCircleCenter.Y; + centerDelta.Z = circleCenter.Z - s.PrevCircleCenter.Z; + useCenterTracking = (pType->Vector_OriginCircleRadius >= 0 || pType->Vector_OriginCircleSpeed != 0 || pType->Vector_OriginCircleAnglePerStep != 0); + } + s.PrevCircleCenter = circleCenter; + + CoordStruct trackPos = currentPos; + if (useCenterTracking) + { + trackPos.X += centerDelta.X; trackPos.Y += centerDelta.Y; trackPos.Z += centerDelta.Z; + } + + double dx = static_cast(trackPos.X - circleCenter.X); + double dy = static_cast(trackPos.Y - circleCenter.Y); + double dz = static_cast(trackPos.Z - circleCenter.Z); + double currentDist; + bool useTiltPlane = hasNormal || (pType->Vector_AllowedTilt && effectiveTilt != 0.0); + + if (useTiltPlane) + { + double cosF = std::cos(effectiveFacing), sinF = std::sin(effectiveFacing); + double cosT = std::cos(effectiveTilt), sinT = std::sin(effectiveTilt); + double dL = dx * (-sinF) + dy * cosF; + double dH = dx * (-cosF * sinT) + dy * (-sinF * sinT) + dz * cosT; + currentDist = std::sqrt(dL * dL + dH * dH); + } + else + { + currentDist = std::sqrt(dx * dx + dy * dy); + } + bool startAtCenter = currentDist < 1.0; + if (currentDist < 1.0) currentDist = 1.0; + + s.CurrentCircleRadius += pType->Vector_CircleRadiusGrow; + double targetRadius = s.CurrentCircleRadius; + if (pType->Vector_CircleMaxRadius > 0 && targetRadius > pType->Vector_CircleMaxRadius) + targetRadius = static_cast(pType->Vector_CircleMaxRadius); + if (pType->Vector_CircleMinRadius > 0 && targetRadius < pType->Vector_CircleMinRadius) + targetRadius = static_cast(pType->Vector_CircleMinRadius); + + if (startAtCenter) + { + dx = targetRadius; dy = 0.0; currentDist = targetRadius; + } + + double rad = V_Deg2Rad(angleStep); + double cosA = std::cos(rad), sinA = std::sin(rad); + CoordStruct moveDisp{ 0, 0, 0 }; + + if (useTiltPlane) + { + double cosF = std::cos(effectiveFacing), sinF = std::sin(effectiveFacing); + double cosT = std::cos(effectiveTilt), sinT = std::sin(effectiveTilt); + double dL = dx * (-sinF) + dy * cosF; + double dH = dx * (-cosF * sinT) + dy * (-sinF * sinT) + dz * cosT; + double curDist = std::sqrt(dL * dL + dH * dH); + if (curDist < 1.0) curDist = 1.0; + double ndL = dL / curDist * targetRadius; + double ndH = dH / curDist * targetRadius; + double rL = ndL * cosA - ndH * sinA; + double rH = ndL * sinA + ndH * cosA; + moveDisp.X = circleCenter.X + static_cast(rL * (-sinF) + rH * (-cosF * sinT)) - currentPos.X; + moveDisp.Y = circleCenter.Y + static_cast(rL * cosF + rH * (-sinF * sinT)) - currentPos.Y; + moveDisp.Z = circleCenter.Z + static_cast(rH * cosT) - currentPos.Z; + } + else + { + double ndx = dx / currentDist * targetRadius; + double ndy = dy / currentDist * targetRadius; + double rx = ndx * cosA - ndy * sinA; + double ry = ndx * sinA + ndy * cosA; + moveDisp.X = circleCenter.X + static_cast(rx) - currentPos.X; + moveDisp.Y = circleCenter.Y + static_cast(ry) - currentPos.Y; + moveDisp.Z = circleCenter.Z - currentPos.Z; + } + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = moveDisp.X; + pB->Velocity.Y = moveDisp.Y; + pB->Velocity.Z = moveDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + + if (pType->Vector_CircleEndOnMaxRadius && pType->Vector_CircleMaxRadius > 0 && s.CurrentCircleRadius >= pType->Vector_CircleMaxRadius) + return; + if (pType->Vector_CircleEndOnMinRadius && pType->Vector_CircleMinRadius > 0 && s.CurrentCircleRadius <= pType->Vector_CircleMinRadius) + return; + + return; +} + +// ================================================================ +// MoveTo mode +// ================================================================ + if (static_cast(pType->Vector_MoveTo) != CoordStruct::Empty) + { + DirStruct moveDir = mainFacingDir; + if (pType->Vector_AnglePerStep != 0.0) + { + if (s.MovementFrames == 1) + s.CurrentAngle = 0.0; + s.CurrentAngle += pType->Vector_AnglePerStep; + moveDir = V_Radians2Dir(mainFacingDir.GetRadian<32>() + V_Deg2Rad(s.CurrentAngle)); + } + + CoordStruct grow{ static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().X * s.MovementFrames : 0), + static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().Y * s.MovementFrames : 0), + static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().Z * s.MovementFrames : 0) }; + CoordStruct mt = pType->Vector_MoveTo; + CoordStruct moveFlh = { mt.X + grow.X, mt.Y + grow.Y, mt.Z + grow.Z }; + + CoordStruct moveDisp = V_FLHAbsoluteOffset(moveFlh, moveDir); + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = moveDisp.X; pB->Velocity.Y = moveDisp.Y; pB->Velocity.Z = moveDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + return; + } + + // ================================================================ + // TargetFLH modes: ReachTarget / Speed + // ================================================================ + if (!pType->Vector_TargetFLH.isset()) + { + if (!isBullet && pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + return; + } + + CoordStruct frameTargetFlh = { pType->Vector_TargetFLH.Get().X + s.TargetOffset.X, + pType->Vector_TargetFLH.Get().Y + s.TargetOffset.Y, + pType->Vector_TargetFLH.Get().Z + s.TargetOffset.Z }; + CoordStruct frameTarget; + + switch (pType->Vector_Origin) + { + case VectorOrigin::Launcher: + if (pInvoker) + frameTarget = TechnoExt::GetFLHAbsoluteCoords(static_cast(pInvoker), frameTargetFlh, !pType->Vector_OriginIsOnBody); + else + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, mainFacingDir); + break; + case VectorOrigin::Self: + if (pType->Vector_OriginIsOnWorld) + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, DirStruct{}); + else if (isBullet) + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(currentPos, frameTargetFlh, mainFacingDir); + else + frameTarget = TechnoExt::GetFLHAbsoluteCoords(static_cast(pObject), frameTargetFlh, !pType->Vector_OriginIsOnBody); + break; + default: + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, mainFacingDir); + break; + } + + CoordStruct dirVec = { frameTarget.X - currentPos.X, frameTarget.Y - currentPos.Y, frameTarget.Z - currentPos.Z }; + double dirLen = std::sqrt(static_cast(dirVec.X * dirVec.X + dirVec.Y * dirVec.Y + dirVec.Z * dirVec.Z)); + CoordStruct resultDisp{ 0, 0, 0 }; + + if (pType->Vector_ReachTarget) + { + int totalDuration = isBullet ? 60 : 60; + int effectiveDuration = totalDuration - pType->Vector_DisabledFrames; + if (effectiveDuration < 1) effectiveDuration = 1; + int remainingFrames = effectiveDuration - s.MovementFrames + 1; + + if (pType->Vector_ReachTargetEarlyEnd > 0 && remainingFrames <= pType->Vector_ReachTargetEarlyEnd) + { + s.DisabledTimer = pType->Vector_ReachTargetEarlyEnd; + return; + } + + if (remainingFrames <= 0) + { + resultDisp.X = frameTarget.X - currentPos.X; + resultDisp.Y = frameTarget.Y - currentPos.Y; + resultDisp.Z = frameTarget.Z - currentPos.Z; + } + else if (dirLen > 1e-6) + { + double adjustedSpeed = dirLen / remainingFrames; + resultDisp.X = static_cast(dirVec.X / dirLen * adjustedSpeed); + resultDisp.Y = static_cast(dirVec.Y / dirLen * adjustedSpeed); + resultDisp.Z = static_cast(dirVec.Z / dirLen * adjustedSpeed); + + if (s.ArcHeight != 0) + { + double t = static_cast(s.MovementFrames - 1) / effectiveDuration; + double tNext = static_cast(s.MovementFrames) / effectiveDuration; + double peakScale = pType->Vector_ArcPeakPercent > 0 ? 1.0 / (pType->Vector_ArcPeakPercent * (1.0 - pType->Vector_ArcPeakPercent)) : 4.0; + double delta = peakScale * s.ArcHeight * (tNext * (1.0 - tNext) - t * (1.0 - t)); + + if (s.ArcRotation == 0.0) + { + resultDisp.Z += static_cast(delta); + } + else + { + double ax = frameTarget.X - s.InitialLocation.X; + double ay = frameTarget.Y - s.InitialLocation.Y; + double az = frameTarget.Z - s.InitialLocation.Z; + double aLen = std::sqrt(ax * ax + ay * ay + az * az); + if (aLen > 1e-6) + { + double dnx = ax / aLen, dny = ay / aLen, dnz = az / aLen; + double upDotD = dnz; + double px = -dnx * upDotD, py = -dny * upDotD, pz = 1.0 - dnz * upDotD; + double pLen = std::sqrt(px * px + py * py + pz * pz); + if (pLen < 1e-6) { px = 1.0 - dnx * dnx; py = -dny * dnx; pz = -dnz * dnx; pLen = std::sqrt(px * px + py * py + pz * pz); } + double pnx = px / pLen, pny = py / pLen, pnz = pz / pLen; + double arcRad = V_Deg2Rad(s.ArcRotation); + double c = std::cos(arcRad), s_arc = std::sin(arcRad); + double rx = pnx * c + (dny * pnz - dnz * pny) * s_arc; + double ry = pny * c + (dnz * pnx - dnx * pnz) * s_arc; + double rz = pnz * c + (dnx * pny - dny * pnx) * s_arc; + resultDisp.X += static_cast(rx * delta); + resultDisp.Y += static_cast(ry * delta); + resultDisp.Z += static_cast(rz * delta); + } + else + { + resultDisp.Z += static_cast(delta); + } + } + } + } + } + else if (dirLen > 1e-6) + { + double spd = s.CurrentSpeed; + if (pType->Vector_Acceleration != 0) spd += pType->Vector_Acceleration * s.CurrentFrame; + if (pType->Vector_MinSpeed >= 0 && spd < pType->Vector_MinSpeed) spd = pType->Vector_MinSpeed; + if (pType->Vector_MaxSpeed >= 0 && spd > pType->Vector_MaxSpeed) spd = pType->Vector_MaxSpeed; + resultDisp.X = static_cast(dirVec.X / dirLen * spd); + resultDisp.Y = static_cast(dirVec.Y / dirLen * spd); + resultDisp.Z = static_cast(dirVec.Z / dirLen * spd); + } + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = resultDisp.X; + pB->Velocity.Y = resultDisp.Y; + pB->Velocity.Z = resultDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + resultDisp.X, currentPos.Y + resultDisp.Y, currentPos.Z + resultDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + + if (!isBullet && pType->Vector_SyncFacing && (resultDisp.X != 0 || resultDisp.Y != 0)) + { + double ang = std::atan2(static_cast(resultDisp.Y), static_cast(resultDisp.X)); + auto dir = DirStruct(static_cast(ang * 32768.0 / 3.14159265358979323846)); + static_cast(pObject)->PrimaryFacing.SetDesired(dir); + } + + if (pType->Vector_AllowFallingDestroy && pType->Vector_FallingDestroyHeight > 0) + { + if (s.InitialLocation.Z - GetPos().Z > pType->Vector_FallingDestroyHeight) + return; + } +} diff --git a/VectorPort/src/New/Entity/VectorState.h b/VectorPort/src/New/Entity/VectorState.h new file mode 100644 index 0000000000..cc08bed4fd --- /dev/null +++ b/VectorPort/src/New/Entity/VectorState.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +#include + +struct VectorState +{ + bool Initialized; + int CurrentFrame; + int DisabledTimer; + double CurrentSpeed; + double CurrentAngle; + double CurrentCircleRadius; + double CurrentCircleSpeed; + double CurrentCircleAngle; + CoordStruct InitialOriginPos; + CoordStruct InitialLocation; + CoordStruct PrevCirclePos; + double ArcHeight; + double ArcRotation; + CoordStruct TargetOffset; + double NormalRotF; + double NormalRotL; + double NormalRotH; + double NormalStepF; + double NormalStepL; + double NormalStepH; + int MovementFrames; + double FacingRad; + double TiltRad; + CoordStruct OriginOffset; + CoordStruct PrevCircleCenter; + int OriginElapsed; + double OriginSpeed; + double OriginAngle; + CoordStruct OriginTargetOffset; + double OriginCircleRadiusRuntime; + double OriginCircleSpeedRuntime; + double OriginCircleAngleRuntime; + double OriginNormalRotFRuntime; + double OriginNormalRotLRuntime; + double OriginNormalRotHRuntime; + double OriginNormalStepF; + double OriginNormalStepL; + double OriginNormalStepH; + double OriginFacing; + double OriginTilt; + + VectorState() + : Initialized { false } + , CurrentFrame { 0 } + , DisabledTimer { 0 } + , CurrentSpeed { 0.0 } + , CurrentAngle { 0.0 } + , CurrentCircleRadius { 0.0 } + , CurrentCircleSpeed { 0.0 } + , CurrentCircleAngle { 0.0 } + , InitialOriginPos { CoordStruct::Empty } + , InitialLocation { CoordStruct::Empty } + , PrevCirclePos { CoordStruct::Empty } + , ArcHeight { 0.0 } + , ArcRotation { 0.0 } + , TargetOffset { CoordStruct::Empty } + , NormalRotF { 0.0 } + , NormalRotL { 0.0 } + , NormalRotH { 0.0 } + , NormalStepF { 0.0 } + , NormalStepL { 0.0 } + , NormalStepH { 0.0 } + , MovementFrames { 0 } + , FacingRad { 0.0 } + , TiltRad { 0.0 } + , OriginOffset { CoordStruct::Empty } + , PrevCircleCenter { CoordStruct::Empty } + , OriginElapsed { 0 } + , OriginSpeed { 0.0 } + , OriginAngle { 0.0 } + , OriginTargetOffset { CoordStruct::Empty } + , OriginCircleRadiusRuntime { 0.0 } + , OriginCircleSpeedRuntime { 0.0 } + , OriginCircleAngleRuntime { 0.0 } + , OriginNormalRotFRuntime { 0.0 } + , OriginNormalRotLRuntime { 0.0 } + , OriginNormalRotHRuntime { 0.0 } + , OriginNormalStepF { 0.0 } + , OriginNormalStepL { 0.0 } + , OriginNormalStepH { 0.0 } + , OriginFacing { 0.0 } + , OriginTilt { 0.0 } + { } +}; + +void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorState& state, ObjectClass* pInvoker, bool isBullet); diff --git a/VectorPort/src/New/Type/AttachEffectTypeClass.cpp b/VectorPort/src/New/Type/AttachEffectTypeClass.cpp new file mode 100644 index 0000000000..76b7d8113a --- /dev/null +++ b/VectorPort/src/New/Type/AttachEffectTypeClass.cpp @@ -0,0 +1,716 @@ +#include "AttachEffectTypeClass.h" + +#include + +// Used to match groups names to AttachEffectTypeClass instances. Do not iterate due to undetermined order being prone to desyncs. +std::unordered_map> AttachEffectTypeClass::GroupsMap; + +bool AttachEffectTypeClass::HasGroup(const std::string& groupID) const +{ + for (auto const& group : this->Groups) + { + if (!group.compare(groupID)) + return true; + } + + return false; +} + +bool AttachEffectTypeClass::HasGroups(const std::vector& groupIDs, bool requireAll) const +{ + size_t foundCount = 0; + + for (auto const& group : this->Groups) + { + for (auto const& requiredGroup : groupIDs) + { + if (!group.compare(requiredGroup)) + { + if (!requireAll) + return true; + + foundCount++; + } + } + } + + return !requireAll ? false : foundCount >= groupIDs.size(); +} + +std::vector AttachEffectTypeClass::GetTypesFromGroups(const std::vector& groupIDs) +{ + std::set types; + auto const map = &AttachEffectTypeClass::GroupsMap; + + for (auto const& group : groupIDs) + { + if (map->contains(group)) + { + auto const values = &map->at(group); + types.insert(values->begin(), values->end()); + } + } + + return std::vector(types.begin(), types.end()); +} + +void AttachEffectTypeClass::HandleEvent(TechnoClass* pTarget) +{ + if (const auto pTag = pTarget->AttachedTag) + pTag->RaiseEvent((TriggerEvent)PhobosTriggerEvent::AttachedIsUnderAttachedEffect, pTarget, CellStruct::Empty); +} + +template<> +const char* Enumerable::GetMainSection() +{ + return "AttachEffectTypes"; +} + +void AttachEffectTypeClass::AddToGroupsMap() +{ + auto const map = &AttachEffectTypeClass::GroupsMap; + + for (auto const& group : this->Groups) + { + if (!map->contains(group)) + { + map->insert(std::make_pair(group, std::set{this})); + } + else + { + auto const values = &map->at(group); + values->insert(this); + } + } +} + +void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) +{ + const char* pSection = this->Name; + + if (INIClass::IsBlank(pSection) || !pINI->GetSection(pSection)) + return; + + INI_EX exINI(pINI); + + this->Duration.Read(exINI, pSection, "Duration"); + this->Duration_ApplyFirepowerMult.Read(exINI, pSection, "Duration.ApplyFirepowerMult"); + this->Duration_ApplyArmorMultOnTarget.Read(exINI, pSection, "Duration.ApplyArmorMultOnTarget"); + this->Cumulative.Read(exINI, pSection, "Cumulative"); + this->Cumulative_MaxCount.Read(exINI, pSection, "Cumulative.MaxCount"); + this->Powered.Read(exINI, pSection, "Powered"); + this->DiscardOn.Read(exINI, pSection, "DiscardOn"); + this->DiscardOn_RangeOverride.Read(exINI, pSection, "DiscardOn.RangeOverride"); + this->DiscardOn_MoveBasedOnDestination.Read(exINI, pSection, "DiscardOn.MoveBasedOnDestination"); + this->PenetratesIronCurtain.Read(exINI, pSection, "PenetratesIronCurtain"); + this->PenetratesForceShield.Read(exINI, pSection, "PenetratesForceShield"); + this->AffectTypes.Read(exINI, pSection, "AffectTypes"); + this->IgnoreTypes.Read(exINI, pSection, "IgnoreTypes"); + if (exINI.ReadString(pSection, "AffectTargets") > 0) + { + Debug::Log("[Developer warning][%s] AffectTargets is deprecated and has been replaced by AffectsTarget! If both are set, the latter will be used.\n", pSection); + } + this->AffectsTarget.Read(exINI, pSection, "AffectTargets"); // Temporary solution for the INI tags renaming issue, see #2093 + this->AffectsTarget.Read(exINI, pSection, "AffectsTarget"); + + this->Animation.Read(exINI, pSection, "Animation"); + this->CumulativeAnimations.Read(exINI, pSection, "CumulativeAnimations"); + this->CumulativeAnimations_RestartOnChange.Read(exINI, pSection, "CumulativeAnimations.RestartOnChange"); + this->Animation_ResetOnReapply.Read(exINI, pSection, "Animation.ResetOnReapply"); + this->Animation_OfflineAction.Read(exINI, pSection, "Animation.OfflineAction"); + this->Animation_TemporalAction.Read(exINI, pSection, "Animation.TemporalAction"); + this->Animation_UseInvokerAsOwner.Read(exINI, pSection, "Animation.UseInvokerAsOwner"); + this->Animation_HideIfAttachedWith.Read(exINI, pSection, "Animation.HideIfAttachedWith"); + + this->ExpireWeapon.Read(exINI, pSection, "ExpireWeapon"); + this->ExpireWeapon_TriggerOn.Read(exINI, pSection, "ExpireWeapon.TriggerOn"); + this->ExpireWeapon_CumulativeOnlyOnce.Read(exINI, pSection, "ExpireWeapon.CumulativeOnlyOnce"); + this->ExpireWeapon_UseInvokerAsOwner.Read(exINI, pSection, "ExpireWeapon.UseInvokerAsOwner"); + this->Next.Read(exINI, pSection, "Next"); + + this->Tint_Color.Read(exINI, pSection, "Tint.Color"); + this->Tint_Intensity.Read(exINI, pSection, "Tint.Intensity"); + this->Tint_VisibleToHouses.Read(exINI, pSection, "Tint.VisibleToHouses"); + + this->FirepowerMultiplier.Read(exINI, pSection, "FirepowerMultiplier"); + this->ArmorMultiplier.Read(exINI, pSection, "ArmorMultiplier"); + this->ArmorMultiplier_AllowWarheads.Read(exINI, pSection, "ArmorMultiplier.AllowWarheads"); + this->ArmorMultiplier_DisallowWarheads.Read(exINI, pSection, "ArmorMultiplier.DisallowWarheads"); + this->SpeedMultiplier.Read(exINI, pSection, "SpeedMultiplier"); + this->ROFMultiplier.Read(exINI, pSection, "ROFMultiplier"); + this->ROFMultiplier_ApplyOnCurrentTimer.Read(exINI, pSection, "ROFMultiplier.ApplyOnCurrentTimer"); + + this->Cloakable.Read(exINI, pSection, "Cloakable"); + this->ForceDecloak.Read(exINI, pSection, "ForceDecloak"); + + this->WeaponRange_Multiplier.Read(exINI, pSection, "WeaponRange.Multiplier"); + this->WeaponRange_ExtraRange.Read(exINI, pSection, "WeaponRange.ExtraRange"); + this->WeaponRange_AllowWeapons.Read(exINI, pSection, "WeaponRange.AllowWeapons"); + this->WeaponRange_DisallowWeapons.Read(exINI, pSection, "WeaponRange.DisallowWeapons"); + + this->Crit_Multiplier.Read(exINI, pSection, "Crit.Multiplier"); + this->Crit_ExtraChance.Read(exINI, pSection, "Crit.ExtraChance"); + this->Crit_AllowWarheads.Read(exINI, pSection, "Crit.AllowWarheads"); + this->Crit_DisallowWarheads.Read(exINI, pSection, "Crit.DisallowWarheads"); + + this->RevengeWeapon.Read(exINI, pSection, "RevengeWeapon"); + if (exINI.ReadString(pSection, "RevengeWeapon.AffectsHouses") > 0) + { + Debug::Log("[Developer warning][%s] RevengeWeapon.AffectsHouses is deprecated and has been replaced by RevengeWeapon.AffectsHouse! If both are set, the latter will be used.\n", pSection); + } + this->RevengeWeapon_AffectsHouse.Read(exINI, pSection, "RevengeWeapon.AffectsHouses"); // Temporary solution for the INI tags renaming issue, see #2093 + this->RevengeWeapon_AffectsHouse.Read(exINI, pSection, "RevengeWeapon.AffectsHouse"); + this->RevengeWeapon_UseInvokerAsOwner.Read(exINI, pSection, "RevengeWeapon.UseInvokerAsOwner"); + + this->ReflectDamage.Read(exINI, pSection, "ReflectDamage"); + this->ReflectDamage_Warhead.Read(exINI, pSection, "ReflectDamage.Warhead"); + this->ReflectDamage_Warhead_Detonate.Read(exINI, pSection, "ReflectDamage.Warhead.Detonate"); + this->ReflectDamage_Multiplier.Read(exINI, pSection, "ReflectDamage.Multiplier"); + if (exINI.ReadString(pSection, "ReflectDamage.AffectsHouses") > 0) + { + Debug::Log("[Developer warning][%s] ReflectDamage.AffectsHouses is deprecated and has been replaced by ReflectDamage.AffectsHouse! If both are set, the latter will be used.\n", pSection); + } + this->ReflectDamage_AffectsHouse.Read(exINI, pSection, "ReflectDamage.AffectsHouses"); // Temporary solution for the INI tags renaming issue, see #2093 + this->ReflectDamage_AffectsHouse.Read(exINI, pSection, "ReflectDamage.AffectsHouse"); + this->ReflectDamage_Chance.Read(exINI, pSection, "ReflectDamage.Chance"); + this->ReflectDamage_Override.Read(exINI, pSection, "ReflectDamage.Override"); + this->ReflectDamage_UseInvokerAsOwner.Read(exINI, pSection, "ReflectDamage.UseInvokerAsOwner"); + + this->DisableWeapons.Read(exINI, pSection, "DisableWeapons"); + this->Unkillable.Read(exINI, pSection, "Unkillable"); + this->LaserTrail_Type.Read(exINI, pSection, "LaserTrail.Type"); + + // Vector + this->Vector_TimeStep.Read(exINI, pSection, "Vector.TimeStep"); + if (this->Vector_TimeStep < 1) + this->Vector_TimeStep = 1; + this->Vector_DisabledFrames.Read(exINI, pSection, "Vector.DisabledFrames"); + this->Vector_SyncFacing.Read(exINI, pSection, "Vector.SyncFacing"); + this->Vector_OriginIsOnWorld.Read(exINI, pSection, "Vector.OriginIsOnWorld"); + this->Vector_OriginIsOnBody.Read(exINI, pSection, "Vector.OriginIsOnBody"); + this->Vector_Origin.Read(exINI, pSection, "Vector.Origin"); + this->Vector_OriginFLH.Read(exINI, pSection, "Vector.OriginFLH"); + this->Vector_OriginNoUpdate.Read(exINI, pSection, "Vector.OriginNoUpdate"); + this->Vector_Force.Read(exINI, pSection, "Vector.Force"); + this->Vector_Freeze.Read(exINI, pSection, "Vector.Freeze"); + this->Vector_AllowedTilt.Read(exINI, pSection, "Vector.AllowedTilt"); + this->Vector_NormalVector.Read(exINI, pSection, "Vector.NormalVector"); + this->Vector_NormalRandomF.Read(exINI, pSection, "Vector.NormalRandomF"); + this->Vector_NormalRandomL.Read(exINI, pSection, "Vector.NormalRandomL"); + this->Vector_NormalRandomH.Read(exINI, pSection, "Vector.NormalRandomH"); + this->Vector_NormalFAnglePerStep.Read(exINI, pSection, "Vector.NormalFAnglePerStep"); + this->Vector_NormalLAnglePerStep.Read(exINI, pSection, "Vector.NormalLAnglePerStep"); + this->Vector_NormalHAnglePerStep.Read(exINI, pSection, "Vector.NormalHAnglePerStep"); + this->Vector_MoveTo.Read(exINI, pSection, "Vector.MoveTo"); + this->Vector_GrowRate.Read(exINI, pSection, "Vector.GrowRate"); + this->Vector_AnglePerStep.Read(exINI, pSection, "Vector.AnglePerStep"); + this->Vector_CircleRadius.Read(exINI, pSection, "Vector.CircleRadius"); + this->Vector_CircleSpeed.Read(exINI, pSection, "Vector.CircleSpeed"); + this->Vector_CircleSpeedAcceleration.Read(exINI, pSection, "Vector.CircleSpeedAcceleration"); + this->Vector_CircleMaxSpeed.Read(exINI, pSection, "Vector.CircleMaxSpeed"); + this->Vector_CircleMinSpeed.Read(exINI, pSection, "Vector.CircleMinSpeed"); + this->Vector_CircleAnglePerStep.Read(exINI, pSection, "Vector.CircleAnglePerStep"); + this->Vector_CircleAngleAcceleration.Read(exINI, pSection, "Vector.CircleAngleAcceleration"); + this->Vector_CircleOrigin.Read(exINI, pSection, "Vector.CircleOrigin"); + this->Vector_AllowOriginTilt.Read(exINI, pSection, "Vector.AllowOriginTilt"); + this->Vector_CircleRadiusGrow.Read(exINI, pSection, "Vector.CircleRadiusGrow"); + this->Vector_CircleMaxRadius.Read(exINI, pSection, "Vector.CircleMaxRadius"); + this->Vector_CircleMinRadius.Read(exINI, pSection, "Vector.CircleMinRadius"); + this->Vector_CircleEndOnMaxRadius.Read(exINI, pSection, "Vector.CircleEndOnMaxRadius"); + this->Vector_CircleEndOnMinRadius.Read(exINI, pSection, "Vector.CircleEndOnMinRadius"); + this->Vector_TargetFLH.Read(exINI, pSection, "Vector.TargetFLH"); + this->Vector_ReachTarget.Read(exINI, pSection, "Vector.ReachTarget"); + this->Vector_ReachTargetEarlyEnd.Read(exINI, pSection, "Vector.ReachTargetEarlyEnd"); + this->Vector_ArcHeight.Read(exINI, pSection, "Vector.ArcHeight"); + this->Vector_ArcPeakPercent.Read(exINI, pSection, "Vector.ArcPeakPercent"); + this->Vector_ArcPeakRandomPercent.Read(exINI, pSection, "Vector.ArcPeakRandomPercent"); + this->Vector_ArcRotation.Read(exINI, pSection, "Vector.ArcRotation"); + this->Vector_InitialSpeed.Read(exINI, pSection, "Vector.InitialSpeed"); + this->Vector_MaxSpeed.Read(exINI, pSection, "Vector.MaxSpeed"); + this->Vector_MinSpeed.Read(exINI, pSection, "Vector.MinSpeed"); + this->Vector_Acceleration.Read(exINI, pSection, "Vector.Acceleration"); + this->Vector_AllowFallingDestroy.Read(exINI, pSection, "Vector.AllowFallingDestroy"); + this->Vector_FallingDestroyHeight.Read(exINI, pSection, "Vector.FallingDestroyHeight"); + this->Vector_AffectTechno.Read(exINI, pSection, "Vector.AffectTechno"); + this->Vector_AffectBullets.Read(exINI, pSection, "Vector.AffectBullets"); + this->Vector_CircleRandomRadiusMin.Read(exINI, pSection, "Vector.CircleRandomRadius.Min"); + this->Vector_CircleRandomRadiusMax.Read(exINI, pSection, "Vector.CircleRandomRadius.Max"); + this->Vector_CircleRandomAngleMin.Read(exINI, pSection, "Vector.CircleRandomAngle.Min"); + this->Vector_CircleRandomAngleMax.Read(exINI, pSection, "Vector.CircleRandomAngle.Max"); + this->Vector_CircleMaxAngle.Read(exINI, pSection, "Vector.CircleMaxAngle"); + this->Vector_CircleMinAngle.Read(exINI, pSection, "Vector.CircleMinAngle"); + this->Vector_TargetOffsetFMin.Read(exINI, pSection, "Vector.TargetOffsetF.Min"); + this->Vector_TargetOffsetFMax.Read(exINI, pSection, "Vector.TargetOffsetF.Max"); + this->Vector_TargetOffsetLMin.Read(exINI, pSection, "Vector.TargetOffsetL.Min"); + this->Vector_TargetOffsetLMax.Read(exINI, pSection, "Vector.TargetOffsetL.Max"); + this->Vector_TargetOffsetHMin.Read(exINI, pSection, "Vector.TargetOffsetH.Min"); + this->Vector_TargetOffsetHMax.Read(exINI, pSection, "Vector.TargetOffsetH.Max"); + this->Vector_ArcRandomHeightMin.Read(exINI, pSection, "Vector.ArcRandomHeight.Min"); + this->Vector_ArcRandomHeightMax.Read(exINI, pSection, "Vector.ArcRandomHeight.Max"); + this->Vector_ArcRandomRotationMin.Read(exINI, pSection, "Vector.ArcRandomRotation.Min"); + this->Vector_ArcRandomRotationMax.Read(exINI, pSection, "Vector.ArcRandomRotation.Max"); + this->Vector_RandomSpeedMin.Read(exINI, pSection, "Vector.RandomSpeed.Min"); + this->Vector_RandomSpeedMax.Read(exINI, pSection, "Vector.RandomSpeed.Max"); + this->Vector_OriginMoveTo.Read(exINI, pSection, "Vector.Origin.MoveTo"); + this->Vector_OriginGrowRate.Read(exINI, pSection, "Vector.Origin.GrowRate"); + this->Vector_OriginTargetFLH.Read(exINI, pSection, "Vector.Origin.TargetFLH"); + this->Vector_OriginInitialSpeed.Read(exINI, pSection, "Vector.Origin.InitialSpeed"); + this->Vector_OriginReachTarget.Read(exINI, pSection, "Vector.Origin.ReachTarget"); + this->Vector_OriginArcHeight.Read(exINI, pSection, "Vector.Origin.ArcHeight"); + this->Vector_OriginCircleRadius.Read(exINI, pSection, "Vector.Origin.CircleRadius"); + this->Vector_OriginCircleSpeed.Read(exINI, pSection, "Vector.Origin.CircleSpeed"); + this->Vector_OriginCircleAnglePerStep.Read(exINI, pSection, "Vector.Origin.CircleAnglePerStep"); + this->Vector_OriginCircleRadiusGrow.Read(exINI, pSection, "Vector.Origin.CircleRadiusGrow"); + this->Vector_OriginCircleMaxRadius.Read(exINI, pSection, "Vector.Origin.CircleMaxRadius"); + this->Vector_OriginCircleMinRadius.Read(exINI, pSection, "Vector.Origin.CircleMinRadius"); + this->Vector_OriginCircleEndOnMaxRadius.Read(exINI, pSection, "Vector.Origin.CircleEndOnMaxRadius"); + this->Vector_OriginCircleEndOnMinRadius.Read(exINI, pSection, "Vector.Origin.CircleEndOnMinRadius"); + this->Vector_OriginNormalVector.Read(exINI, pSection, "Vector.Origin.NormalVector"); + this->Vector_OriginNormalFAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalFAnglePerStep"); + this->Vector_OriginNormalLAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalLAnglePerStep"); + this->Vector_OriginNormalHAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalHAnglePerStep"); + this->Vector_OriginAllowedTilt.Read(exINI, pSection, "Vector.Origin.AllowedTilt"); + this->Vector_OriginCircleOffset.Read(exINI, pSection, "Vector.Origin.CircleOffset"); + this->Vector_OriginOrigin.Read(exINI, pSection, "Vector.Origin.Origin"); + this->Vector_OriginOriginFLH.Read(exINI, pSection, "Vector.Origin.OriginFLH"); + this->Vector_NormalFAngleRMin.Read(exINI, pSection, "Vector.NormalFAngleRanges.Min"); + this->Vector_NormalFAngleRMax.Read(exINI, pSection, "Vector.NormalFAngleRanges.Max"); + this->Vector_NormalFAngleRMin2.Read(exINI, pSection, "Vector.NormalFAngleRanges.Min2"); + this->Vector_NormalFAngleRMax2.Read(exINI, pSection, "Vector.NormalFAngleRanges.Max2"); + this->Vector_NormalLAngleRMin.Read(exINI, pSection, "Vector.NormalLAngleRanges.Min"); + this->Vector_NormalLAngleRMax.Read(exINI, pSection, "Vector.NormalLAngleRanges.Max"); + this->Vector_NormalLAngleRMin2.Read(exINI, pSection, "Vector.NormalLAngleRanges.Min2"); + this->Vector_NormalLAngleRMax2.Read(exINI, pSection, "Vector.NormalLAngleRanges.Max2"); + this->Vector_NormalHAngleRMin.Read(exINI, pSection, "Vector.NormalHAngleRanges.Min"); + this->Vector_NormalHAngleRMax.Read(exINI, pSection, "Vector.NormalHAngleRanges.Max"); + this->Vector_NormalHAngleRMin2.Read(exINI, pSection, "Vector.NormalHAngleRanges.Min2"); + this->Vector_NormalHAngleRMax2.Read(exINI, pSection, "Vector.NormalHAngleRanges.Max2"); + this->Vector_OriginNormalFAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Min"); + this->Vector_OriginNormalFAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Max"); + this->Vector_OriginNormalFAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Min2"); + this->Vector_OriginNormalFAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Max2"); + this->Vector_OriginNormalLAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Min"); + this->Vector_OriginNormalLAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Max"); + this->Vector_OriginNormalLAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Min2"); + this->Vector_OriginNormalLAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Max2"); + this->Vector_OriginNormalHAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Min"); + this->Vector_OriginNormalHAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Max"); + this->Vector_OriginNormalHAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Min2"); + this->Vector_OriginNormalHAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Max2"); + + // Groups + exINI.ParseStringList(this->Groups, pSection, "Groups"); + AddToGroupsMap(); +} + +template +void AttachEffectTypeClass::Serialize(T& Stm) +{ + Stm + .Process(this->Duration) + .Process(this->Duration_ApplyFirepowerMult) + .Process(this->Duration_ApplyArmorMultOnTarget) + .Process(this->Cumulative) + .Process(this->Cumulative_MaxCount) + .Process(this->Powered) + .Process(this->DiscardOn) + .Process(this->DiscardOn_RangeOverride) + .Process(this->DiscardOn_MoveBasedOnDestination) + .Process(this->PenetratesIronCurtain) + .Process(this->PenetratesForceShield) + .Process(this->AffectTypes) + .Process(this->IgnoreTypes) + .Process(this->AffectsTarget) + .Process(this->Animation) + .Process(this->CumulativeAnimations) + .Process(this->CumulativeAnimations_RestartOnChange) + .Process(this->Animation_ResetOnReapply) + .Process(this->Animation_OfflineAction) + .Process(this->Animation_TemporalAction) + .Process(this->Animation_UseInvokerAsOwner) + .Process(this->Animation_HideIfAttachedWith) + .Process(this->ExpireWeapon) + .Process(this->ExpireWeapon_TriggerOn) + .Process(this->ExpireWeapon_CumulativeOnlyOnce) + .Process(this->ExpireWeapon_UseInvokerAsOwner) + .Process(this->Next) + .Process(this->Tint_Color) + .Process(this->Tint_Intensity) + .Process(this->Tint_VisibleToHouses) + .Process(this->FirepowerMultiplier) + .Process(this->ArmorMultiplier) + .Process(this->ArmorMultiplier_AllowWarheads) + .Process(this->ArmorMultiplier_DisallowWarheads) + .Process(this->SpeedMultiplier) + .Process(this->ROFMultiplier) + .Process(this->ROFMultiplier_ApplyOnCurrentTimer) + .Process(this->Cloakable) + .Process(this->ForceDecloak) + .Process(this->WeaponRange_Multiplier) + .Process(this->WeaponRange_ExtraRange) + .Process(this->WeaponRange_AllowWeapons) + .Process(this->WeaponRange_DisallowWeapons) + .Process(this->Crit_Multiplier) + .Process(this->Crit_ExtraChance) + .Process(this->Crit_AllowWarheads) + .Process(this->Crit_DisallowWarheads) + .Process(this->RevengeWeapon) + .Process(this->RevengeWeapon_AffectsHouse) + .Process(this->RevengeWeapon_UseInvokerAsOwner) + .Process(this->ReflectDamage) + .Process(this->ReflectDamage_Warhead) + .Process(this->ReflectDamage_Warhead_Detonate) + .Process(this->ReflectDamage_Multiplier) + .Process(this->ReflectDamage_AffectsHouse) + .Process(this->ReflectDamage_Chance) + .Process(this->ReflectDamage_Override) + .Process(this->ReflectDamage_UseInvokerAsOwner) + .Process(this->DisableWeapons) + .Process(this->Unkillable) + .Process(this->LaserTrail_Type) + .Process(this->Vector_TimeStep) + .Process(this->Vector_DisabledFrames) + .Process(this->Vector_SyncFacing) + .Process(this->Vector_OriginIsOnWorld) + .Process(this->Vector_OriginIsOnBody) + .Process(this->Vector_Origin) + .Process(this->Vector_OriginFLH) + .Process(this->Vector_OriginNoUpdate) + .Process(this->Vector_Force) + .Process(this->Vector_Freeze) + .Process(this->Vector_AllowedTilt) + .Process(this->Vector_NormalVector) + .Process(this->Vector_NormalRandomF) + .Process(this->Vector_NormalRandomL) + .Process(this->Vector_NormalRandomH) + .Process(this->Vector_NormalFAnglePerStep) + .Process(this->Vector_NormalLAnglePerStep) + .Process(this->Vector_NormalHAnglePerStep) + .Process(this->Vector_MoveTo) + .Process(this->Vector_GrowRate) + .Process(this->Vector_AnglePerStep) + .Process(this->Vector_CircleRadius) + .Process(this->Vector_CircleSpeed) + .Process(this->Vector_CircleSpeedAcceleration) + .Process(this->Vector_CircleMaxSpeed) + .Process(this->Vector_CircleMinSpeed) + .Process(this->Vector_CircleAnglePerStep) + .Process(this->Vector_CircleAngleAcceleration) + .Process(this->Vector_CircleOrigin) + .Process(this->Vector_AllowOriginTilt) + .Process(this->Vector_CircleRadiusGrow) + .Process(this->Vector_CircleMaxRadius) + .Process(this->Vector_CircleMinRadius) + .Process(this->Vector_CircleEndOnMaxRadius) + .Process(this->Vector_CircleEndOnMinRadius) + .Process(this->Vector_TargetFLH) + .Process(this->Vector_ReachTarget) + .Process(this->Vector_ReachTargetEarlyEnd) + .Process(this->Vector_ArcHeight) + .Process(this->Vector_ArcPeakPercent) + .Process(this->Vector_ArcPeakRandomPercent) + .Process(this->Vector_ArcRotation) + .Process(this->Vector_InitialSpeed) + .Process(this->Vector_MaxSpeed) + .Process(this->Vector_MinSpeed) + .Process(this->Vector_Acceleration) + .Process(this->Vector_AllowFallingDestroy) + .Process(this->Vector_FallingDestroyHeight) + .Process(this->Vector_AffectTechno) + .Process(this->Vector_AffectBullets) + .Process(this->Vector_CircleRandomRadiusMin) + .Process(this->Vector_CircleRandomRadiusMax) + .Process(this->Vector_CircleRandomAngleMin) + .Process(this->Vector_CircleRandomAngleMax) + .Process(this->Vector_CircleMaxAngle) + .Process(this->Vector_CircleMinAngle) + .Process(this->Vector_TargetOffsetFMin) + .Process(this->Vector_TargetOffsetFMax) + .Process(this->Vector_TargetOffsetLMin) + .Process(this->Vector_TargetOffsetLMax) + .Process(this->Vector_TargetOffsetHMin) + .Process(this->Vector_TargetOffsetHMax) + .Process(this->Vector_ArcRandomHeightMin) + .Process(this->Vector_ArcRandomHeightMax) + .Process(this->Vector_ArcRandomRotationMin) + .Process(this->Vector_ArcRandomRotationMax) + .Process(this->Vector_RandomSpeedMin) + .Process(this->Vector_RandomSpeedMax) + .Process(this->Vector_OriginMoveTo) + .Process(this->Vector_OriginGrowRate) + .Process(this->Vector_OriginTargetFLH) + .Process(this->Vector_OriginInitialSpeed) + .Process(this->Vector_OriginReachTarget) + .Process(this->Vector_OriginArcHeight) + .Process(this->Vector_OriginCircleRadius) + .Process(this->Vector_OriginCircleSpeed) + .Process(this->Vector_OriginCircleAnglePerStep) + .Process(this->Vector_OriginCircleRadiusGrow) + .Process(this->Vector_OriginCircleMaxRadius) + .Process(this->Vector_OriginCircleMinRadius) + .Process(this->Vector_OriginCircleEndOnMaxRadius) + .Process(this->Vector_OriginCircleEndOnMinRadius) + .Process(this->Vector_OriginNormalVector) + .Process(this->Vector_OriginNormalFAnglePerStep) + .Process(this->Vector_OriginNormalLAnglePerStep) + .Process(this->Vector_OriginNormalHAnglePerStep) + .Process(this->Vector_OriginAllowedTilt) + .Process(this->Vector_OriginCircleOffset) + .Process(this->Vector_OriginOrigin) + .Process(this->Vector_OriginOriginFLH) + .Process(this->Vector_NormalFAngleRMin) + .Process(this->Vector_NormalFAngleRMax) + .Process(this->Vector_NormalFAngleRMin2) + .Process(this->Vector_NormalFAngleRMax2) + .Process(this->Vector_NormalLAngleRMin) + .Process(this->Vector_NormalLAngleRMax) + .Process(this->Vector_NormalLAngleRMin2) + .Process(this->Vector_NormalLAngleRMax2) + .Process(this->Vector_NormalHAngleRMin) + .Process(this->Vector_NormalHAngleRMax) + .Process(this->Vector_NormalHAngleRMin2) + .Process(this->Vector_NormalHAngleRMax2) + .Process(this->Vector_OriginNormalFAngleRMin) + .Process(this->Vector_OriginNormalFAngleRMax) + .Process(this->Vector_OriginNormalFAngleRMin2) + .Process(this->Vector_OriginNormalFAngleRMax2) + .Process(this->Vector_OriginNormalLAngleRMin) + .Process(this->Vector_OriginNormalLAngleRMax) + .Process(this->Vector_OriginNormalLAngleRMin2) + .Process(this->Vector_OriginNormalLAngleRMax2) + .Process(this->Vector_OriginNormalHAngleRMin) + .Process(this->Vector_OriginNormalHAngleRMax) + .Process(this->Vector_OriginNormalHAngleRMin2) + .Process(this->Vector_OriginNormalHAngleRMax2) + .Process(this->Groups) + ; +} + +void AttachEffectTypeClass::LoadFromStream(PhobosStreamReader& Stm) +{ + this->Serialize(Stm); + AddToGroupsMap(); +} + +void AttachEffectTypeClass::SaveToStream(PhobosStreamWriter& Stm) +{ + this->Serialize(Stm); +} + +// AE type-related enum etc. parsers +namespace detail +{ + template <> + inline bool read(DiscardCondition& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto parsed = DiscardCondition::None; + + auto str = parser.value(); + char* context = nullptr; + for (auto cur = strtok_s(str, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + if (!_strcmpi(cur, "none")) + { + parsed |= DiscardCondition::None; + } + else if (!_strcmpi(cur, "entry")) + { + parsed |= DiscardCondition::Entry; + } + else if (!_strcmpi(cur, "move")) + { + parsed |= DiscardCondition::Move; + } + else if (!_strcmpi(cur, "stationary")) + { + parsed |= DiscardCondition::Stationary; + } + else if (!_strcmpi(cur, "drain")) + { + parsed |= DiscardCondition::Drain; + } + else if (!_strcmpi(cur, "inrange")) + { + parsed |= DiscardCondition::InRange; + } + else if (!_strcmpi(cur, "outofrange")) + { + parsed |= DiscardCondition::OutOfRange; + } + else if (!_strcmpi(cur, "firing")) + { + parsed |= DiscardCondition::Firing; + } + else + { + Debug::INIParseFailed(pSection, pKey, cur, "Expected a discard condition type"); + return false; + } + } + + value = parsed; + return true; + } + + return false; + } + + template <> + inline bool read(ExpireWeaponCondition& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto parsed = ExpireWeaponCondition::None; + + auto str = parser.value(); + char* context = nullptr; + for (auto cur = strtok_s(str, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + if (!_strcmpi(cur, "none")) + { + parsed |= ExpireWeaponCondition::None; + } + else if (!_strcmpi(cur, "expire")) + { + parsed |= ExpireWeaponCondition::Expire; + } + else if (!_strcmpi(cur, "remove")) + { + parsed |= ExpireWeaponCondition::Remove; + } + else if (!_strcmpi(cur, "death")) + { + parsed |= ExpireWeaponCondition::Death; + } + else if (!_strcmpi(cur, "discard")) + { + parsed |= ExpireWeaponCondition::Discard; + } + else if (!_strcmpi(cur, "all")) + { + parsed |= ExpireWeaponCondition::All; + } + else + { + Debug::INIParseFailed(pSection, pKey, cur, "Expected a expire weapon trigger condition type"); + return false; + } + } + + value = parsed; + return true; + } + + return false; + } + + template <> + inline bool read(VectorOrigin& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto str = parser.value(); + + if (!_strcmpi(str, "Self")) + value = VectorOrigin::Self; + else if (!_strcmpi(str, "Launcher")) + value = VectorOrigin::Launcher; + else if (!_strcmpi(str, "Target")) + value = VectorOrigin::Target; + else if (!_strcmpi(str, "Source")) + value = VectorOrigin::Source; + else + value = VectorOrigin::Self; + + return true; + } + + return false; + } +} + +// AEAttachInfoTypeClass + +void AEAttachInfoTypeClass::LoadFromINI(CCINIClass* pINI, const char* pSection) +{ + INI_EX exINI(pINI); + + this->AttachTypes.Read(exINI, pSection, "AttachEffect.AttachTypes"); + this->CumulativeSourceMaxCount.Read(exINI, pSection, "AttachEffect.CumulativeSourceMaxCount"); + this->CumulativeRefreshAll.Read(exINI, pSection, "AttachEffect.CumulativeRefreshAll"); + this->CumulativeRefreshAll_OnAttach.Read(exINI, pSection, "AttachEffect.CumulativeRefreshAll.OnAttach"); + this->CumulativeRefreshSameSourceOnly.Read(exINI, pSection, "AttachEffect.CumulativeRefreshSameSourceOnly"); + this->RemoveTypes.Read(exINI, pSection, "AttachEffect.RemoveTypes"); + exINI.ParseStringList(this->RemoveGroups, pSection, "AttachEffect.RemoveGroups"); + this->CumulativeRemoveMinCounts.Read(exINI, pSection, "AttachEffect.CumulativeRemoveMinCounts"); + this->CumulativeRemoveMaxCounts.Read(exINI, pSection, "AttachEffect.CumulativeRemoveMaxCounts"); + this->DurationOverrides.Read(exINI, pSection, "AttachEffect.DurationOverrides"); + this->Delays.Read(exINI, pSection, "AttachEffect.Delays"); + this->InitialDelays.Read(exINI, pSection, "AttachEffect.InitialDelays"); + this->RecreationDelays.Read(exINI, pSection, "AttachEffect.RecreationDelays"); +} + +AEAttachParams AEAttachInfoTypeClass::GetAttachParams(unsigned int index, bool selfOwned) const +{ + AEAttachParams info { }; + + if (this->DurationOverrides.size() > 0) + info.DurationOverride = this->DurationOverrides[this->DurationOverrides.size() > index ? index : this->DurationOverrides.size() - 1]; + + if (selfOwned) + { + if (this->Delays.size() > 0) + info.Delay = this->Delays[this->Delays.size() > index ? index : this->Delays.size() - 1]; + + if (this->InitialDelays.size() > 0) + info.InitialDelay = this->InitialDelays[this->InitialDelays.size() > index ? index : this->InitialDelays.size() - 1]; + + if (this->RecreationDelays.size() > 0) + info.RecreationDelay = this->RecreationDelays[this->RecreationDelays.size() > index ? index : this->RecreationDelays.size() - 1]; + } + else + { + info.CumulativeSourceMaxCount = this->CumulativeSourceMaxCount; + info.CumulativeRefreshAll = this->CumulativeRefreshAll; + info.CumulativeRefreshAll_OnAttach = this->CumulativeRefreshAll_OnAttach; + info.CumulativeRefreshSameSourceOnly = this->CumulativeRefreshSameSourceOnly; + } + + return info; +} + +#pragma region(save/load) + +template +bool AEAttachInfoTypeClass::Serialize(T& stm) +{ + return stm + .Process(this->AttachTypes) + .Process(this->CumulativeSourceMaxCount) + .Process(this->CumulativeRefreshAll) + .Process(this->CumulativeRefreshAll_OnAttach) + .Process(this->CumulativeRefreshSameSourceOnly) + .Process(this->RemoveTypes) + .Process(this->RemoveGroups) + .Process(this->CumulativeRemoveMinCounts) + .Process(this->CumulativeRemoveMaxCounts) + .Process(this->DurationOverrides) + .Process(this->Delays) + .Process(this->InitialDelays) + .Process(this->RecreationDelays) + .Success(); +} + +bool AEAttachInfoTypeClass::Load(PhobosStreamReader& stm, bool registerForChange) +{ + return this->Serialize(stm); +} + +bool AEAttachInfoTypeClass::Save(PhobosStreamWriter& stm) const +{ + return const_cast(this)->Serialize(stm); +} + +#pragma endregion(save/load) diff --git a/VectorPort/src/New/Type/AttachEffectTypeClass.h b/VectorPort/src/New/Type/AttachEffectTypeClass.h new file mode 100644 index 0000000000..5f4c624bfd --- /dev/null +++ b/VectorPort/src/New/Type/AttachEffectTypeClass.h @@ -0,0 +1,529 @@ +#pragma once + +#include +#include + +#include +#include +#include "LaserTrailTypeClass.h" + +// AE discard condition +enum class DiscardCondition : unsigned char +{ + None = 0x0, + Entry = 0x1, + Move = 0x2, + Stationary = 0x4, + Drain = 0x8, + InRange = 0x10, + OutOfRange = 0x20, + Firing = 0x40 +}; + +MAKE_ENUM_FLAGS(DiscardCondition); + +// AE expire weapon condition +enum class ExpireWeaponCondition : unsigned char +{ + None = 0x0, + Expire = 0x1, + Remove = 0x2, + Death = 0x4, + Discard = 0x8, + + All = 0xFF, +}; + +MAKE_ENUM_FLAGS(ExpireWeaponCondition); + +enum class VectorOrigin : int +{ + Self = 0, + Launcher = 1, + Target = 2, + Source = 3 +}; + +class AttachEffectTypeClass final : public Enumerable +{ + static std::unordered_map> GroupsMap; + +public: + Valueable Duration; + Valueable Duration_ApplyFirepowerMult; + Valueable Duration_ApplyArmorMultOnTarget; + Valueable Cumulative; + Valueable Cumulative_MaxCount; + Valueable Powered; + Valueable DiscardOn; + Nullable DiscardOn_RangeOverride; + Nullable DiscardOn_MoveBasedOnDestination; + Valueable PenetratesIronCurtain; + Nullable PenetratesForceShield; + ValueableVector AffectTypes; + ValueableVector IgnoreTypes; + Valueable AffectsTarget; + Valueable Animation; + ValueableVector CumulativeAnimations; + Valueable CumulativeAnimations_RestartOnChange; + Valueable Animation_ResetOnReapply; + Valueable Animation_OfflineAction; + Valueable Animation_TemporalAction; + Valueable Animation_UseInvokerAsOwner; + ValueableVector Animation_HideIfAttachedWith; + Valueable ExpireWeapon; + Valueable ExpireWeapon_TriggerOn; + Valueable ExpireWeapon_CumulativeOnlyOnce; + Valueable ExpireWeapon_UseInvokerAsOwner; + ValueableVector Next; + Nullable Tint_Color; + Valueable Tint_Intensity; + Valueable Tint_VisibleToHouses; + Valueable FirepowerMultiplier; + Valueable ArmorMultiplier; + ValueableVector ArmorMultiplier_AllowWarheads; + ValueableVector ArmorMultiplier_DisallowWarheads; + Valueable SpeedMultiplier; + Valueable ROFMultiplier; + Valueable ROFMultiplier_ApplyOnCurrentTimer; + Valueable Cloakable; + Valueable ForceDecloak; + Valueable WeaponRange_Multiplier; + Valueable WeaponRange_ExtraRange; + ValueableVector WeaponRange_AllowWeapons; + ValueableVector WeaponRange_DisallowWeapons; + Valueable Crit_Multiplier; + Valueable Crit_ExtraChance; + ValueableVector Crit_AllowWarheads; + ValueableVector Crit_DisallowWarheads; + Valueable RevengeWeapon; + Valueable RevengeWeapon_AffectsHouse; + Valueable RevengeWeapon_UseInvokerAsOwner; + Valueable ReflectDamage; + Nullable ReflectDamage_Warhead; + Valueable ReflectDamage_Warhead_Detonate; + Valueable ReflectDamage_Multiplier; + Valueable ReflectDamage_AffectsHouse; + Valueable ReflectDamage_Chance; + Nullable ReflectDamage_Override; + Valueable ReflectDamage_UseInvokerAsOwner; + Valueable DisableWeapons; + Valueable Unkillable; + ValueableIdx LaserTrail_Type; + + Valueable Vector_TimeStep; + Valueable Vector_DisabledFrames; + Valueable Vector_SyncFacing; + Valueable Vector_OriginIsOnWorld; + Valueable Vector_OriginIsOnBody; + Valueable Vector_Origin; + Nullable Vector_OriginFLH; + Valueable Vector_OriginNoUpdate; + Valueable Vector_Force; + Valueable Vector_Freeze; + Valueable Vector_AllowedTilt; + Nullable Vector_NormalVector; + Valueable Vector_NormalRandomF; + Valueable Vector_NormalRandomL; + Valueable Vector_NormalRandomH; + Valueable Vector_NormalFAnglePerStep; + Valueable Vector_NormalLAnglePerStep; + Valueable Vector_NormalHAnglePerStep; + Valueable Vector_MoveTo; + Nullable Vector_GrowRate; + Valueable Vector_AnglePerStep; + Valueable Vector_CircleRadius; + Valueable Vector_CircleSpeed; + Valueable Vector_CircleSpeedAcceleration; + Valueable Vector_CircleMaxSpeed; + Valueable Vector_CircleMinSpeed; + Valueable Vector_CircleAnglePerStep; + Valueable Vector_CircleAngleAcceleration; + Nullable Vector_CircleOrigin; + Valueable Vector_AllowOriginTilt; + Valueable Vector_CircleRadiusGrow; + Valueable Vector_CircleMaxRadius; + Valueable Vector_CircleMinRadius; + Valueable Vector_CircleEndOnMaxRadius; + Valueable Vector_CircleEndOnMinRadius; + Nullable Vector_TargetFLH; + Valueable Vector_ReachTarget; + Valueable Vector_ReachTargetEarlyEnd; + Valueable Vector_ArcHeight; + Valueable Vector_ArcPeakPercent; + Valueable Vector_ArcPeakRandomPercent; + Valueable Vector_ArcRotation; + Valueable Vector_InitialSpeed; + Valueable Vector_MaxSpeed; + Valueable Vector_MinSpeed; + Valueable Vector_Acceleration; + Valueable Vector_AllowFallingDestroy; + Valueable Vector_FallingDestroyHeight; + Valueable Vector_AffectTechno; + Valueable Vector_AffectBullets; + Valueable Vector_CircleRandomRadiusMin; + Valueable Vector_CircleRandomRadiusMax; + Valueable Vector_CircleRandomAngleMin; + Valueable Vector_CircleRandomAngleMax; + Valueable Vector_CircleMaxAngle; + Valueable Vector_CircleMinAngle; + Valueable Vector_TargetOffsetFMin; + Valueable Vector_TargetOffsetFMax; + Valueable Vector_TargetOffsetLMin; + Valueable Vector_TargetOffsetLMax; + Valueable Vector_TargetOffsetHMin; + Valueable Vector_TargetOffsetHMax; + Valueable Vector_ArcRandomHeightMin; + Valueable Vector_ArcRandomHeightMax; + Valueable Vector_ArcRandomRotationMin; + Valueable Vector_ArcRandomRotationMax; + Valueable Vector_RandomSpeedMin; + Valueable Vector_RandomSpeedMax; + Nullable Vector_OriginMoveTo; + Nullable Vector_OriginGrowRate; + Nullable Vector_OriginTargetFLH; + Valueable Vector_OriginInitialSpeed; + Valueable Vector_OriginReachTarget; + Valueable Vector_OriginArcHeight; + Valueable Vector_OriginCircleRadius; + Valueable Vector_OriginCircleSpeed; + Valueable Vector_OriginCircleAnglePerStep; + Valueable Vector_OriginCircleRadiusGrow; + Valueable Vector_OriginCircleMaxRadius; + Valueable Vector_OriginCircleMinRadius; + Valueable Vector_OriginCircleEndOnMaxRadius; + Valueable Vector_OriginCircleEndOnMinRadius; + Nullable Vector_OriginNormalVector; + Valueable Vector_OriginNormalFAnglePerStep; + Valueable Vector_OriginNormalLAnglePerStep; + Valueable Vector_OriginNormalHAnglePerStep; + Valueable Vector_OriginAllowedTilt; + Nullable Vector_OriginCircleOffset; + Valueable Vector_OriginOrigin; + Nullable Vector_OriginOriginFLH; + Valueable Vector_NormalFAngleRMin; + Valueable Vector_NormalFAngleRMax; + Valueable Vector_NormalFAngleRMin2; + Valueable Vector_NormalFAngleRMax2; + Valueable Vector_NormalLAngleRMin; + Valueable Vector_NormalLAngleRMax; + Valueable Vector_NormalLAngleRMin2; + Valueable Vector_NormalLAngleRMax2; + Valueable Vector_NormalHAngleRMin; + Valueable Vector_NormalHAngleRMax; + Valueable Vector_NormalHAngleRMin2; + Valueable Vector_NormalHAngleRMax2; + Valueable Vector_OriginNormalFAngleRMin; + Valueable Vector_OriginNormalFAngleRMax; + Valueable Vector_OriginNormalFAngleRMin2; + Valueable Vector_OriginNormalFAngleRMax2; + Valueable Vector_OriginNormalLAngleRMin; + Valueable Vector_OriginNormalLAngleRMax; + Valueable Vector_OriginNormalLAngleRMin2; + Valueable Vector_OriginNormalLAngleRMax2; + Valueable Vector_OriginNormalHAngleRMin; + Valueable Vector_OriginNormalHAngleRMax; + Valueable Vector_OriginNormalHAngleRMin2; + Valueable Vector_OriginNormalHAngleRMax2; + + std::vector Groups; + + AttachEffectTypeClass(const char* const pTitle) : Enumerable(pTitle) + , Duration { 0 } + , Duration_ApplyFirepowerMult { false } + , Duration_ApplyArmorMultOnTarget { false } + , Cumulative { false } + , Cumulative_MaxCount { -1 } + , Powered { false } + , DiscardOn { DiscardCondition::None } + , DiscardOn_RangeOverride {} + , DiscardOn_MoveBasedOnDestination {} + , PenetratesIronCurtain { false } + , PenetratesForceShield {} + , AffectTypes {} + , IgnoreTypes {} + , AffectsTarget { AffectedTarget::All } + , Animation {} + , CumulativeAnimations {} + , CumulativeAnimations_RestartOnChange { true } + , Animation_ResetOnReapply { false } + , Animation_OfflineAction { AttachedAnimFlag::Hides } + , Animation_TemporalAction { AttachedAnimFlag::None } + , Animation_UseInvokerAsOwner { false } + , Animation_HideIfAttachedWith {} + , ExpireWeapon {} + , ExpireWeapon_TriggerOn { ExpireWeaponCondition::Expire } + , ExpireWeapon_CumulativeOnlyOnce { false } + , ExpireWeapon_UseInvokerAsOwner { false } + , Next {} + , Tint_Color {} + , Tint_Intensity { 0.0 } + , Tint_VisibleToHouses { AffectedHouse::All } + , FirepowerMultiplier { 1.0 } + , ArmorMultiplier { 1.0 } + , ArmorMultiplier_AllowWarheads {} + , ArmorMultiplier_DisallowWarheads {} + , SpeedMultiplier { 1.0 } + , ROFMultiplier { 1.0 } + , ROFMultiplier_ApplyOnCurrentTimer { true } + , Cloakable { false } + , ForceDecloak { false } + , WeaponRange_Multiplier { 1.0 } + , WeaponRange_ExtraRange { 0.0 } + , WeaponRange_AllowWeapons {} + , WeaponRange_DisallowWeapons {} + , Crit_Multiplier { 1.0 } + , Crit_ExtraChance { 0.0 } + , Crit_AllowWarheads {} + , Crit_DisallowWarheads {} + , RevengeWeapon {} + , RevengeWeapon_AffectsHouse { AffectedHouse::All } + , ReflectDamage { false } + , RevengeWeapon_UseInvokerAsOwner { false } + , ReflectDamage_Warhead {} + , ReflectDamage_Warhead_Detonate { false } + , ReflectDamage_Multiplier { 1.0 } + , ReflectDamage_AffectsHouse { AffectedHouse::All } + , ReflectDamage_Chance { 1.0 } + , ReflectDamage_Override {} + , ReflectDamage_UseInvokerAsOwner { false } + , DisableWeapons { false } + , Unkillable { false } + , LaserTrail_Type { -1 } + , Vector_TimeStep { 1 } + , Vector_DisabledFrames { 0 } + , Vector_SyncFacing { true } + , Vector_OriginIsOnWorld { false } + , Vector_OriginIsOnBody { false } + , Vector_Origin { VectorOrigin::Self } + , Vector_OriginFLH {} + , Vector_OriginNoUpdate { false } + , Vector_Force { true } + , Vector_Freeze { false } + , Vector_AllowedTilt { false } + , Vector_NormalVector {} + , Vector_NormalRandomF { 0.0 } + , Vector_NormalRandomL { 0.0 } + , Vector_NormalRandomH { 0.0 } + , Vector_NormalFAnglePerStep { 0.0 } + , Vector_NormalLAnglePerStep { 0.0 } + , Vector_NormalHAnglePerStep { 0.0 } + , Vector_MoveTo { CoordStruct::Empty } + , Vector_GrowRate {} + , Vector_AnglePerStep { 0.0 } + , Vector_CircleRadius { -1 } + , Vector_CircleSpeed { 0 } + , Vector_CircleSpeedAcceleration { 0 } + , Vector_CircleMaxSpeed { 0 } + , Vector_CircleMinSpeed { 0 } + , Vector_CircleAnglePerStep { 0.0 } + , Vector_CircleAngleAcceleration { 0.0 } + , Vector_CircleOrigin {} + , Vector_AllowOriginTilt { false } + , Vector_CircleRadiusGrow { 0 } + , Vector_CircleMaxRadius { 0 } + , Vector_CircleMinRadius { 0 } + , Vector_CircleEndOnMaxRadius { false } + , Vector_CircleEndOnMinRadius { false } + , Vector_TargetFLH {} + , Vector_ReachTarget { false } + , Vector_ReachTargetEarlyEnd { 0 } + , Vector_ArcHeight { 0 } + , Vector_ArcPeakPercent { 0.0 } + , Vector_ArcPeakRandomPercent { 0.0 } + , Vector_ArcRotation { 0.0 } + , Vector_InitialSpeed { -1 } + , Vector_MaxSpeed { -1 } + , Vector_MinSpeed { -1 } + , Vector_Acceleration { 0 } + , Vector_AllowFallingDestroy { false } + , Vector_FallingDestroyHeight { 0 } + , Vector_AffectTechno { true } + , Vector_AffectBullets { false } + , Vector_CircleRandomRadiusMin { -1 } + , Vector_CircleRandomRadiusMax { -1 } + , Vector_CircleRandomAngleMin { -1 } + , Vector_CircleRandomAngleMax { -1 } + , Vector_CircleMaxAngle { 0.0 } + , Vector_CircleMinAngle { 0.0 } + , Vector_TargetOffsetFMin { 0 } + , Vector_TargetOffsetFMax { 0 } + , Vector_TargetOffsetLMin { 0 } + , Vector_TargetOffsetLMax { 0 } + , Vector_TargetOffsetHMin { 0 } + , Vector_TargetOffsetHMax { 0 } + , Vector_ArcRandomHeightMin { 0 } + , Vector_ArcRandomHeightMax { 0 } + , Vector_ArcRandomRotationMin { 0.0 } + , Vector_ArcRandomRotationMax { 0.0 } + , Vector_RandomSpeedMin { -1 } + , Vector_RandomSpeedMax { -1 } + , Vector_OriginMoveTo {} + , Vector_OriginGrowRate {} + , Vector_OriginTargetFLH {} + , Vector_OriginInitialSpeed { -1 } + , Vector_OriginReachTarget { false } + , Vector_OriginArcHeight { 0 } + , Vector_OriginCircleRadius { -1 } + , Vector_OriginCircleSpeed { 0 } + , Vector_OriginCircleAnglePerStep { 0.0 } + , Vector_OriginCircleRadiusGrow { 0 } + , Vector_OriginCircleMaxRadius { 0 } + , Vector_OriginCircleMinRadius { 0 } + , Vector_OriginCircleEndOnMaxRadius { false } + , Vector_OriginCircleEndOnMinRadius { false } + , Vector_OriginNormalVector {} + , Vector_OriginNormalFAnglePerStep { 0.0 } + , Vector_OriginNormalLAnglePerStep { 0.0 } + , Vector_OriginNormalHAnglePerStep { 0.0 } + , Vector_OriginAllowedTilt { false } + , Vector_OriginCircleOffset {} + , Vector_OriginOrigin { VectorOrigin::Self } + , Vector_OriginOriginFLH {} + , Vector_NormalFAngleRMin { 0.0 } + , Vector_NormalFAngleRMax { 0.0 } + , Vector_NormalFAngleRMin2 { 0.0 } + , Vector_NormalFAngleRMax2 { 0.0 } + , Vector_NormalLAngleRMin { 0.0 } + , Vector_NormalLAngleRMax { 0.0 } + , Vector_NormalLAngleRMin2 { 0.0 } + , Vector_NormalLAngleRMax2 { 0.0 } + , Vector_NormalHAngleRMin { 0.0 } + , Vector_NormalHAngleRMax { 0.0 } + , Vector_NormalHAngleRMin2 { 0.0 } + , Vector_NormalHAngleRMax2 { 0.0 } + , Vector_OriginNormalFAngleRMin { 0.0 } + , Vector_OriginNormalFAngleRMax { 0.0 } + , Vector_OriginNormalFAngleRMin2 { 0.0 } + , Vector_OriginNormalFAngleRMax2 { 0.0 } + , Vector_OriginNormalLAngleRMin { 0.0 } + , Vector_OriginNormalLAngleRMax { 0.0 } + , Vector_OriginNormalLAngleRMin2 { 0.0 } + , Vector_OriginNormalLAngleRMax2 { 0.0 } + , Vector_OriginNormalHAngleRMin { 0.0 } + , Vector_OriginNormalHAngleRMax { 0.0 } + , Vector_OriginNormalHAngleRMin2 { 0.0 } + , Vector_OriginNormalHAngleRMax2 { 0.0 } + , Groups {} + {}; + + bool HasTint() const + { + return this->Tint_Color.isset() || this->Tint_Intensity != 0.0; + } + + bool HasVector() const + { + return static_cast(this->Vector_MoveTo) != CoordStruct::Empty + || this->Vector_TargetFLH.isset() + || this->Vector_Freeze + || this->Vector_ReachTarget + || this->Vector_CircleRadius > 0 + || this->Vector_CircleSpeed != 0 + || this->Vector_CircleAnglePerStep > 0.0 + || (this->Vector_CircleRandomRadiusMax > this->Vector_CircleRandomRadiusMin) + || (this->Vector_CircleRandomAngleMax > this->Vector_CircleRandomAngleMin) + || this->Vector_OriginMoveTo.isset(); + } + + bool HasGroup(const std::string& groupID) const; + bool HasGroups(const std::vector& groupIDs, bool requireAll) const; + + AnimTypeClass* GetCumulativeAnimation(int cumulativeCount) const + { + if (cumulativeCount < 0 || this->CumulativeAnimations.size() < 1) + return nullptr; + + const int index = static_cast(cumulativeCount) >= this->CumulativeAnimations.size() ? this->CumulativeAnimations.size() - 1 : cumulativeCount - 1; + + return this->CumulativeAnimations.at(index); + } + + void LoadFromINI(CCINIClass* pINI); + void LoadFromStream(PhobosStreamReader& Stm); + void SaveToStream(PhobosStreamWriter& Stm); + + static void Clear() + { + AttachEffectTypeClass::GroupsMap.clear(); + } + + static std::vector GetTypesFromGroups(const std::vector& groupIDs); + static void HandleEvent(TechnoClass* pTarget); + +private: + template + void Serialize(T& Stm); + void AddToGroupsMap(); +}; + +// Container for AttachEffect attachment for an individual effect passed to AE attach function. +struct AEAttachParams +{ + int DurationOverride; + int Delay; + int InitialDelay; + int RecreationDelay; + int CumulativeSourceMaxCount; + bool CumulativeRefreshAll; + bool CumulativeRefreshAll_OnAttach; + bool CumulativeRefreshSameSourceOnly; + + AEAttachParams() : + DurationOverride { 0 } + , Delay { 0 } + , InitialDelay { 0 } + , RecreationDelay { -1 } + , CumulativeSourceMaxCount { -1 } + , CumulativeRefreshAll { false } + , CumulativeRefreshAll_OnAttach { false } + , CumulativeRefreshSameSourceOnly { true } + { + } +}; + +// Container for AttachEffect attachment info parsed from INI. +class AEAttachInfoTypeClass +{ +public: + ValueableVector AttachTypes; + Valueable CumulativeSourceMaxCount; + Valueable CumulativeRefreshAll; + Valueable CumulativeRefreshAll_OnAttach; + Valueable CumulativeRefreshSameSourceOnly; + ValueableVector RemoveTypes; + std::vector RemoveGroups; + ValueableVector CumulativeRemoveMinCounts; + ValueableVector CumulativeRemoveMaxCounts; + ValueableVector DurationOverrides; + ValueableVector Delays; + ValueableVector InitialDelays; + ValueableVector RecreationDelays; + + void LoadFromINI(CCINIClass* pINI, const char* pSection); + bool Load(PhobosStreamReader& stm, bool registerForChange); + bool Save(PhobosStreamWriter& stm) const; + + AEAttachParams GetAttachParams(unsigned int index, bool selfOwned) const; + + AEAttachInfoTypeClass() : + AttachTypes {} + , CumulativeSourceMaxCount { -1 } + , CumulativeRefreshAll { false } + , CumulativeRefreshAll_OnAttach { false } + , CumulativeRefreshSameSourceOnly { true } + , RemoveTypes {} + , RemoveGroups {} + , CumulativeRemoveMinCounts {} + , CumulativeRemoveMaxCounts {} + , DurationOverrides {} + , Delays {} + , InitialDelays {} + , RecreationDelays {} + { + } + +private: + template + bool Serialize(T& stm); +}; diff --git a/Vector_FixList.md b/Vector_FixList.md new file mode 100644 index 0000000000..d06d9978f1 --- /dev/null +++ b/Vector_FixList.md @@ -0,0 +1,153 @@ +# Phobos Vector 待修问题清单 +> 基于 KratosAI `src/Ext/EffectType/Effect/VectorEffect.cpp` 与 PhobosAI `src/New/Entity/VectorState.cpp` 逐行对比。 +> 所有行号以当前 feature/vector-port 分支为准。 + +--- + +## 🔴 B1 — OriginNormalVector atan2 参数反了 +**文件**: `src/New/Entity/VectorState.cpp:419` +**问题**: `std::atan2(nv.Y, nv.X)` — 参数顺序是 `atan2(L, F)`,应为 `atan2(F, L)`。 +**参考 Kratos**: `VectorEffect.cpp:587` — `std::atan2(fy, fx)` where `fy = OriginNormalVector.X (F), fx = .Y (L)` +**修复**: 改为 `std::atan2(nv.X, nv.Y)` +**影响**: 原点法向量旋转 90°,圆心方向错误。 + +--- + +## 🔴 B2 — pInvoker 存活检查缺失 +**文件**: `src/New/Entity/VectorState.cpp:256` +**问题**: `auto const pLT = static_cast(pInvoker)` 无 null/存活检查,pInvoker 死亡时直接崩溃。 +**参考 Kratos**: `VectorEffect.cpp:400-402` — `if (_pLauncher && !IsDeadOrInvisible(_pLauncher)) { TechnoClass* pLauncherTechno = abstract_cast(_pLauncher); }` +**修复**: 在 `static_cast` 前加 `if (pInvoker && !pInvoker->IsAlive)` 守卫,或换成 `abstract_cast`。 + +--- + +## 🔴 B3 — 动态 facing 地面子弹无 TargetCoords 回退 +**文件**: `src/New/Entity/VectorState.cpp:211` +**问题**: `if (pB->Target)` 只检查 Techno* 目标,地面对 TargetCoords 无回退。 +**参考 Kratos**: `VectorEffect.cpp:364-371` — 区分 ground 和 Techno,ground 用 `TargetCoords`。 +**修复**: 改为 `pB->Target ? pB->Target->GetCoords() : pB->TargetCoords` + +--- + +## 🟡 B4 — Origin=Launcher/Target/Source 初始 facing 缺失 +**文件**: `src/New/Entity/VectorState.cpp:87-104` +**问题**: 初始化时统一用 bullet velocity 或 PrimaryFacing,未按 Origin 类型分别设置 facing。 +**参考 Kratos**: `VectorEffect.cpp:180-259` — 完整的 switch(Origin) 分支,Launcher 用发射者朝向,Target 用自→目标方向,Source 用源→自方向,均含 AllowedTilt tilt 计算。 +**修复**: 在 `s.Initialized` 块中,模仿 Kratos 的 switch 分支设置 `s.FacingRad` 和 `s.TiltRad`(用于 OriginNoUpdate 或无 NormalVector 的初始 facing)。 + +--- + +## 🟡 B5 — 初始速度无 techno/bullet 回退 +**文件**: `src/New/Entity/VectorState.cpp:106` +**问题**: `InitialSpeed >= 0 ? InitialSpeed : 0` — 默认 0,不读单位/弹体原生速度。 +**参考 Kratos**: `VectorEffect.cpp:60-71` — 未设 InitialSpeed 时回退到 `pType->JumpjetSpeed / Speed` 或 `pBullet->Speed`。 +**修复**: InitialSpeed < 0 时从 pObject 读原生速度。 + +--- + +## 🟡 B6 — Self 模式 InitialFacing 用 Primary 而非 Turret +**文件**: `src/New/Entity/VectorState.cpp:100` +**问题**: `pT->PrimaryFacing.Current().GetRadian<32>()` 应改为 `TurretFacing()`. +**参考 Kratos**: `VectorEffect.cpp:389` — 有 `OriginIsOnBody` 控制,默认 TurretFacing。 +**修复**: 如 OriginIsOnBody=false,用 `pT->TurretFacing().Current()`。 + +--- + +## 🟡 B7 — remainingFrames 差 1 帧 +**文件**: `src/New/Entity/VectorState.cpp:687` +**问题**: `effectiveDuration - s.MovementFrames` 应改为 `effectiveDuration - s.MovementFrames + 1`。 +**参考 Kratos**: `VectorEffect.cpp:930` — `effectiveDuration - _movementFrames + 1`。 +**修复**: 加 `+1`。 + +--- + +## 🟡 B8 — Source+OriginNoUpdate 初始位置未初始化 +**文件**: `src/New/Entity/VectorState.cpp:58-70` +**问题**: `s.InitialOriginPos` 只处理了 Launcher 和 Target 分支,无 Source 分支。 +**参考 Kratos**: `VectorEffect.cpp:116-124` — Source 时读 `AE->pSource->GetCoords()`。 +**修复**: 添加 `case VectorOrigin::Source:` 分支,从 pInvoker (对子弹即为 AE source) 取坐标。 + +--- + +## 🟠 M1 — 圆随机半径用 int trunc +**文件**: `src/New/Entity/VectorState.cpp:325` +**问题**: `rand() % (Max - Min + 1)` 整数截断。 +**参考 Kratos**: `VectorEffect.cpp:780` — `Random::RandomRanged` (double)。 +**修复**: 改用 `V_Random(Min, Max)`。 + +--- + +## 🟠 M2 — OriginLissajous 缺失 +**文件**: `src/New/Entity/VectorState.cpp:428-442` (圆形移动段) +**问题**: Phobos 始终用 smooth 增量旋转,无 Lissajous 模式。 +**参考 Kratos**: `VectorEffect.cpp:703-704` — `Data->OriginLissajous ? cumulativeAngle : incrementOnly`。 +**修复**: 添加 `Vector_OriginLissajous` 标签,按 Kratos 逻辑分支处理。 + +--- + +## 🟠 M3 — Origin ReachTarget + ArcHeight 缺失 +**文件**: `src/New/Entity/VectorState.cpp:407-455` +**问题**: 圆心只支持 Speed 模式(恒速追踪),无 ReachTarget 模式及弧高。 +**参考 Kratos**: `VectorEffect.cpp:654-679` — OriginReachTarget 分支含 remainingFrames 分批到达及 `OriginArcHeight` 弧高计算。 +**修复**: 添加完整的 OriginReachTarget 分支代码。 + +--- + +## 🟠 M4 — OriginCircleOffset 缺失 +**问题**: Phobos 无 `Vector_OriginCircleOffset` 标签。 +**参考 Kratos**: `VectorEffect.cpp:559-560` — `baseCenter += Data->OriginCircleOffset` (世界偏移)。 +**修复**: 新增标签及读取,在 baseCenter 计算后叠加。 + +--- + +## 🟠 M5 — OriginTargetOffset 随机化缺失 +**文件**: `src/New/Entity/VectorState.cpp:413` +**问题**: `s.OriginTargetOffset = CoordStruct::Empty` 未赋值。 +**参考 Kratos**: `VectorEffect.cpp:576-578` — 每轴独立随机范围。 +**修复**: 初始化时按 `OriginTargetOffsetF/L/H Min/Max` 随机赋值。 + +--- + +## 🟠 M6 — Origin 加速度/钳位缺失 +**问题**: Phobos 圆心速度恒定为 InitialSpeed,无加速度和上下限。 +**参考 Kratos**: `VectorEffect.cpp:683-685` — `OriginAcceleration`、`OriginMaxSpeed`、`OriginMinSpeed`。 +**修复**: 新增标签,Speed 模式每帧叠加加速度并钳位。 + +--- + +## 🟠 M7 — NormalCircleRadius 哨兵缺失 +**问题**: 有 NormalVector 但无明确 CircleRadius 时不会自动推断。 +**参考 Kratos**: `VectorEffect.cpp:595-596` — 设 `_originCircleRadius = effectiveFacing > -1 ? 0 : -1` 作为哨兵。 +**修复**: 在初始化时加类似哨兵逻辑。 + +--- + +## 🟠 M8 — ReachTargetEarlyEnd 死循环 +**文件**: `src/New/Entity/VectorState.cpp:689-693` +**问题**: DisabledTimer 归零后 remainingFrames 不变,反复触发 EarlyEnd。 +**参考 Kratos**: `VectorEffect.cpp:934` — `Deactivate()` 直接终止,不设 timer。 +**修复**: 改为一次性 `s.DisabledTimer = EarlyEnd` 后立即递增 `s.MovementFrames` 以退出循环区间,或直接 `Deactivate()`。 + +--- + +## 🟠 M9 — 建筑对象无排除 +**文件**: `src/New/Entity/VectorState.cpp:33` (VectorAI_Run 入口) +**问题**: 无 building 类型检查。 +**参考 Kratos**: `VectorEffect.cpp:19-23` — Building 对象直接 `Deactivate()`。 +**修复**: 入口加 `if (pObject->WhatAmI() == AbstractType::Building) return;` + +--- + +## 🟠 M10 — 圆模式双区间随机角速度缺失 +**问题**: Phobos 只有单区间 `CircleRandomAngleMin/Max`。 +**参考 Kratos**: `VectorEffect.cpp:485-489` — 4 值区间 `CircleRandomAngleMin/Max` + `Min2/Max2`。 +**修复**: 新增标签及初始化时的 4 值随机逻辑。 + +--- + +## 📋 修复顺序建议 +1. 先修 🔴 B1~B3(崩溃/功能错误) +2. 再修 🟡 B4~B8(行为偏差) +3. 最后补 🟠 M1~M10(缺失功能) + +每次修改后验证编译 `scripts\build_debug.bat` 通过。 diff --git a/phobosvector.ini b/phobosvector.ini new file mode 100644 index 0000000000..294fbe9a66 --- /dev/null +++ b/phobosvector.ini @@ -0,0 +1,127 @@ + ================================================================ + Phobos Vector 标签一览 + [SomeVectorType] + Duration=N AE 持续时间 (帧) + Next=AnotherVectorType 串联下一个 Vector (过期后自动切换) + ================================================================ + + --- 通用 --- + Vector.TimeStep=1 每 N 帧运动一次 + Vector.DisabledFrames=0 首帧快照后冻结 N 帧 + Vector.SyncFacing=yes 抛射体面朝运动方向 / 单位转动 + Vector.OriginIsOnWorld=no yes=世界坐标系 FLH (朝北) + Vector.OriginIsOnBody=no yes=取车身 PrimaryFacing 无视炮塔 + Vector.Origin=Self Self | Launcher | Target + Vector.OriginFLH=0,0,0 Origin 偏移 + Vector.OriginNoUpdate=no yes=锁定初始坐标不刷新 + Vector.Force=yes yes=SetLocation 硬控 + Vector.Freeze=no yes=冻结原地 + Vector.AllowedTilt=no yes=允许坐标系倾斜 + + --- NormalVector (圆法向量, FLH 坐标系) --- + Vector.NormalVector=F,L,H 法向量 F/L/H 分量 + Vector.NormalRandomF.Min=0 F 分量随机下限 + Vector.NormalRandomF.Max=0 F 分量随机上限 + Vector.NormalRandomL.Min=0 + Vector.NormalRandomL.Max=0 + Vector.NormalRandomH.Min=0 + Vector.NormalRandomH.Max=0 + Vector.NormalFAnglePerStep=0 绕 F 轴角速度 (°/step) + Vector.NormalLAnglePerStep=0 + Vector.NormalHAnglePerStep=0 + Vector.NormalFAngleRanges.Min=0 绕 F 轴角速度 4 值区间 + Vector.NormalFAngleRanges.Max=0 + Vector.NormalFAngleRanges.Min2=0 + Vector.NormalFAngleRanges.Max2=0 + (同理 .NormalLAngleRanges / .NormalHAngleRanges) + + --- MoveTo 模式 --- + Vector.MoveTo=X,Y,Z 纯 FLH 位移 / step + Vector.GrowRate=X,Y,Z MoveTo 每帧增量 + Vector.AnglePerStep=0 MoveTo 自旋角度 (°/step) + + --- Circle 模式 --- + Vector.CircleRadius=-1 半径 (-1=取当前 XY 距离) + Vector.CircleSpeed=0 线速度 + Vector.CircleSpeedAcceleration=0 线速度加速度 + Vector.CircleMaxSpeed=0 线速度上限 (0=不限) + Vector.CircleMinSpeed=0 + Vector.CircleAnglePerStep=0 角速度 (°/step) + Vector.CircleAngleAcceleration=0 角速度加速度 + Vector.CircleMaxAngle=0 角速度上限 (0=不限) + Vector.CircleMinAngle=0 + Vector.CircleOrigin=F,L,H 圆心偏移 (默认世界坐标, AllowOriginTilt 时 FLH 旋转) + Vector.AllowOriginTilt=0 yes=圆心偏移跟随倾斜 + Vector.CircleRadiusGrow=0 半径每帧增长 (正=外扩) + Vector.CircleMaxRadius=0 半径上限 + Vector.CircleMinRadius=0 半径下限 + Vector.CircleEndOnMaxRadius=no 到达上限时结束 + Vector.CircleEndOnMinRadius=no + Vector.CircleRandomRadius.Min=0 随机半径范围 + Vector.CircleRandomRadius.Max=0 + Vector.CircleRandomAngle.Min=0 随机角速度范围 + Vector.CircleRandomAngle.Max=0 + + --- Origin 圆心移动 (Circle 模式专用) --- + Vector.Origin.MoveTo=F,L,H 圆心 FLH 位移 + Vector.Origin.GrowRate=F,L,H 每帧增量 + Vector.Origin.AnglePerStep=0 自旋角度 + Vector.Origin.TargetFLH=F,L,H 追踪目标 FLH + Vector.Origin.InitialSpeed=-1 初始速度 (-1=读单位 Speed) + Vector.Origin.Acceleration=0 加速度 + Vector.Origin.MaxSpeed=-1 (-1=不限) + Vector.Origin.MinSpeed=-1 + Vector.Origin.ReachTarget=no 到达模式 + Vector.Origin.ArcHeight=0 到达模式弧高 + Vector.Origin.CircleRadius=-1 + Vector.Origin.CircleSpeed=0 + Vector.Origin.CircleAnglePerStep=0 + Vector.Origin.CircleRadiusGrow=0 + Vector.Origin.CircleMaxRadius=0 + Vector.Origin.CircleMinRadius=0 + Vector.Origin.CircleEndOnMaxRadius=no + Vector.Origin.CircleEndOnMinRadius=no + Vector.Origin.NormalVector=F,L,H 圆心法向量 + Vector.Origin.NormalFAnglePerStep=0 + Vector.Origin.NormalLAnglePerStep=0 + Vector.Origin.NormalHAnglePerStep=0 + Vector.Origin.AllowedTilt=no + Vector.Origin.CircleOffset=X,Y,Z 原点世界偏移 + Vector.Origin.Origin=Self 参考系 Self|Launcher|Target|Source + Vector.Origin.OriginFLH=F,L,H + Vector.Origin .NormalFAngleRanges.Min/Min2 等同上 + + --- Speed / ReachTarget 模式 --- + Vector.TargetFLH=F,L,H 目标 FLH 偏移 + Vector.TargetOffsetF.Min=0 F 方向随机偏移范围 + Vector.TargetOffsetF.Max=0 + Vector.TargetOffsetL.Min=0 + Vector.TargetOffsetL.Max=0 + Vector.TargetOffsetH.Min=0 + Vector.TargetOffsetH.Max=0 + Vector.InitialSpeed=-1 初始速度 (-1=读单位 Speed) + Vector.RandomSpeed.Min=0 随机速度范围 + Vector.RandomSpeed.Max=0 + Vector.MaxSpeed=-1 (-1=不限) + Vector.MinSpeed=-1 + Vector.Acceleration=0 加速度 (每帧) + Vector.ReachTarget=no yes=剩余帧数强制到达 + Vector.ReachTargetEarlyEnd=0 提前 N 帧结束 (触发 Next) + + --- 弧高 (ReachTarget 模式) --- + Vector.ArcHeight=0 弧高 (lepton), 0=直线 + Vector.ArcRandomHeight.Min=0 随机弧高范围 + Vector.ArcRandomHeight.Max=0 + Vector.ArcPeakPercent=50 弧顶 Duration 百分比 (0-100) + Vector.ArcPeakRandomPercent=0 固定随机峰位 + Vector.ArcPeakRandomPercent.Min=0 随机峰位范围 + Vector.ArcPeakRandomPercent.Max=0 + Vector.ArcRotation=0 弧面旋转角 (°) + Vector.ArcRandomRotation.Min=0 + Vector.ArcRandomRotation.Max=0 + + --- 杂项 --- + Vector.AllowFallingDestroy=no 结束时摔死 + Vector.FallingDestroyHeight=208 摔死高度阈值 + Vector.AffectTechno=yes + Vector.AffectBullets=no diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp index ff66cb5236..9ab91a2548 100644 --- a/src/New/Entity/VectorState.cpp +++ b/src/New/Entity/VectorState.cpp @@ -4,17 +4,17 @@ #include #include -static void VecLog(const char* fmt, ...) -{ - va_list args; - va_start(args, fmt); - FILE* f = fopen("vec_debug.log", "a"); - if (f) { - vfprintf(f, fmt, args); - fclose(f); - } - va_end(args); -} +//static void VecLog(const char* fmt, ...) +//{ +// va_list args; +// va_start(args, fmt); +// FILE* f = fopen("vec_debug.log", "a"); +// if (f) { +// vfprintf(f, fmt, args); +// fclose(f); +// } +// va_end(args); +//} static double V_Random(double min, double max) { @@ -449,10 +449,10 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat if (hasOriginSub) { - VecLog("[ORIGIN] Frame=%d MOVFRM=%d CR=%d CSP=%d CAng=%.1f\n", - Unsorted::CurrentFrame, s.MovementFrames, - (int)pType->Vector_OriginCircleRadius, (int)pType->Vector_OriginCircleSpeed, - pType->Vector_OriginCircleAnglePerStep); + //VecLog("[ORIGIN] Frame=%d MOVFRM=%d CR=%d CSP=%d CAng=%.1f\n", + // Unsorted::CurrentFrame, s.MovementFrames, + // (int)pType->Vector_OriginCircleRadius, (int)pType->Vector_OriginCircleSpeed, + // pType->Vector_OriginCircleAnglePerStep); CoordStruct baseCenter = originPos; if (pType->Vector_OriginOrigin != VectorOrigin::Self) @@ -646,11 +646,11 @@ void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorStat s.OriginOffset.X += disp.X; s.OriginOffset.Y += disp.Y; s.OriginOffset.Z += disp.Z; circleCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; - VecLog("[ORIGIN] of=%.3f ot=%.3f rad=%.1f ang=%.1f disp=(%d,%d,%d) offset=(%d,%d,%d) center=(%d,%d,%d)\n", - oFacing, oTilt, s.OriginCircleRadiusRuntime, s.OriginCircleAngleRuntime, - disp.X, disp.Y, disp.Z, - s.OriginOffset.X, s.OriginOffset.Y, s.OriginOffset.Z, - circleCenter.X, circleCenter.Y, circleCenter.Z); + //VecLog("[ORIGIN] of=%.3f ot=%.3f rad=%.1f ang=%.1f disp=(%d,%d,%d) offset=(%d,%d,%d) center=(%d,%d,%d)\n", + // oFacing, oTilt, s.OriginCircleRadiusRuntime, s.OriginCircleAngleRuntime, + // disp.X, disp.Y, disp.Z, + // s.OriginOffset.X, s.OriginOffset.Y, s.OriginOffset.Z, + // circleCenter.X, circleCenter.Y, circleCenter.Z); s.OriginElapsed++; } diff --git a/src/New/Entity/VectorState.cpp.circlebackup b/src/New/Entity/VectorState.cpp.circlebackup new file mode 100644 index 0000000000..74abef89a1 --- /dev/null +++ b/src/New/Entity/VectorState.cpp.circlebackup @@ -0,0 +1,757 @@ +#include "VectorState.h" +#include "AttachEffectClass.h" +#include +#include + +static double V_Random(double min, double max) +{ + if (min >= max) return min; + return min + (max - min) * static_cast(std::rand()) / RAND_MAX; +} + +static double V_Deg2Rad(double deg) { return deg * 3.14159265358979323846 / 180.0; } +static double V_Rad2Deg(double rad) { return rad * 180.0 / 3.14159265358979323846; } + +static DirStruct V_Radians2Dir(double rad) +{ + return DirStruct(static_cast(rad * 32768.0 / 3.14159265358979323846)); +} + +static DirStruct V_Point2Dir(CoordStruct from, CoordStruct to) +{ + double dx = to.X - from.X, dy = to.Y - from.Y; + return V_Radians2Dir(std::atan2(dy, dx)); +} + +static CoordStruct V_FLHAbsoluteOffset(CoordStruct flh, DirStruct facing) +{ + return AttachEffectClass::GetFLHAbsoluteCoords(CoordStruct::Empty, flh, facing); +} + +void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorState& s, ObjectClass* pInvoker, bool isBullet) +{ + if (!pObject) + return; + + if (s.DisabledTimer > 0) + { + s.DisabledTimer--; + return; + } + + bool skipFrame = pType->Vector_TimeStep > 1 && (s.CurrentFrame % pType->Vector_TimeStep != 0); + s.CurrentFrame++; + + if (skipFrame) + return; + + s.MovementFrames++; + s.NormalRotF += s.NormalStepF; + s.NormalRotL += s.NormalStepL; + s.NormalRotH += s.NormalStepH; + + auto GetPos = [&]() { return pObject->GetCoords(); }; + + if (!s.Initialized) + { + s.Initialized = true; + s.InitialOriginPos = GetPos(); + s.InitialLocation = GetPos(); + + if (isBullet) + { + auto const pB = static_cast(pObject); + double vx = pB->Velocity.X, vy = pB->Velocity.Y; + s.FacingRad = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + } + else + { + auto const pT = static_cast(pObject); + s.FacingRad = pT->PrimaryFacing.Current().GetRadian<32>(); + } + s.TiltRad = 0.0; + s.OriginFacing = s.FacingRad; + s.OriginTilt = 0.0; + + double speed = static_cast(pType->Vector_InitialSpeed >= 0 ? pType->Vector_InitialSpeed : 0); + if (pType->Vector_RandomSpeedMin != pType->Vector_RandomSpeedMax) + speed = V_Random(pType->Vector_RandomSpeedMin, pType->Vector_RandomSpeedMax); + s.CurrentSpeed = speed; + + s.ArcHeight = static_cast(pType->Vector_ArcHeight); + if (pType->Vector_ArcRandomHeightMin != pType->Vector_ArcRandomHeightMax) + s.ArcHeight = V_Random(pType->Vector_ArcRandomHeightMin, pType->Vector_ArcRandomHeightMax); + s.ArcRotation = pType->Vector_ArcRotation; + if (pType->Vector_ArcRandomRotationMin != pType->Vector_ArcRandomRotationMax) + s.ArcRotation = V_Random(pType->Vector_ArcRandomRotationMin, pType->Vector_ArcRandomRotationMax); + + if (pType->Vector_TargetFLH.isset()) + { + CoordStruct offset = pType->Vector_TargetFLH.Get(); + if (pType->Vector_TargetOffsetFMin != pType->Vector_TargetOffsetFMax) + offset.X = pType->Vector_TargetOffsetFMin + rand() % (pType->Vector_TargetOffsetFMax - pType->Vector_TargetOffsetFMin + 1); + if (pType->Vector_TargetOffsetLMin != pType->Vector_TargetOffsetLMax) + offset.Y = pType->Vector_TargetOffsetLMin + rand() % (pType->Vector_TargetOffsetLMax - pType->Vector_TargetOffsetLMin + 1); + if (pType->Vector_TargetOffsetHMin != pType->Vector_TargetOffsetHMax) + offset.Z = pType->Vector_TargetOffsetHMin + rand() % (pType->Vector_TargetOffsetHMax - pType->Vector_TargetOffsetHMin + 1); + s.TargetOffset = offset; + } + + if (pType->Vector_NormalVector.isset()) + { + s.NormalRotF = 0.0; s.NormalRotL = 0.0; s.NormalRotH = 0.0; + auto ns = [](double ps, double m1, double M1, double m2, double M2) { + if (ps != 0.0) return ps; + if (M2 > m2 && (rand() % 2)) return m2 + (rand() / (double)RAND_MAX) * (M2 - m2); + return M1 > m1 ? m1 + (rand() / (double)RAND_MAX) * (M1 - m1) : 0.0; + }; + s.NormalStepF = ns(pType->Vector_NormalFAnglePerStep, pType->Vector_NormalFAngleRMin, pType->Vector_NormalFAngleRMax, pType->Vector_NormalFAngleRMin2, pType->Vector_NormalFAngleRMax2); + s.NormalStepL = ns(pType->Vector_NormalLAnglePerStep, pType->Vector_NormalLAngleRMin, pType->Vector_NormalLAngleRMax, pType->Vector_NormalLAngleRMin2, pType->Vector_NormalLAngleRMax2); + s.NormalStepH = ns(pType->Vector_NormalHAnglePerStep, pType->Vector_NormalHAngleRMin, pType->Vector_NormalHAngleRMax, pType->Vector_NormalHAngleRMin2, pType->Vector_NormalHAngleRMax2); + } + + if (pType->Vector_OriginNormalVector.isset()) + { + auto ns = [](double ps, double m1, double M1, double m2, double M2) { + if (ps != 0.0) return ps; + if (M2 > m2 && (rand() % 2)) return m2 + (rand() / (double)RAND_MAX) * (M2 - m2); + return M1 > m1 ? m1 + (rand() / (double)RAND_MAX) * (M1 - m1) : 0.0; + }; + s.OriginNormalStepF = ns(pType->Vector_OriginNormalFAnglePerStep, pType->Vector_OriginNormalFAngleRMin, pType->Vector_OriginNormalFAngleRMax, pType->Vector_OriginNormalFAngleRMin2, pType->Vector_OriginNormalFAngleRMax2); + s.OriginNormalStepL = ns(pType->Vector_OriginNormalLAnglePerStep, pType->Vector_OriginNormalLAngleRMin, pType->Vector_OriginNormalLAngleRMax, pType->Vector_OriginNormalLAngleRMin2, pType->Vector_OriginNormalLAngleRMax2); + s.OriginNormalStepH = ns(pType->Vector_OriginNormalHAnglePerStep, pType->Vector_OriginNormalHAngleRMin, pType->Vector_OriginNormalHAngleRMax, pType->Vector_OriginNormalHAngleRMin2, pType->Vector_OriginNormalHAngleRMax2); + } + else + { + s.OriginNormalStepF = pType->Vector_OriginNormalFAnglePerStep; + s.OriginNormalStepL = pType->Vector_OriginNormalLAnglePerStep; + s.OriginNormalStepH = pType->Vector_OriginNormalHAnglePerStep; + } + } + + // === Dynamic facing === + double effectiveFacing = s.FacingRad + V_Deg2Rad(s.NormalRotH); + double effectiveTilt = s.TiltRad + V_Deg2Rad(s.NormalRotL); + DirStruct mainFacingDir = V_Radians2Dir(effectiveFacing); + + if (pType->Vector_OriginIsOnWorld) + { + mainFacingDir = DirStruct{}; + effectiveFacing = 0.0; + effectiveTilt = 0.0; + } + + bool hasNormal = pType->Vector_NormalVector.isset(); + if (!pType->Vector_OriginNoUpdate && !hasNormal && !pType->Vector_OriginIsOnWorld) + { + switch (pType->Vector_Origin) + { + case VectorOrigin::Source: + if (pInvoker) + { + mainFacingDir = V_Point2Dir(pInvoker->GetCoords(), GetPos()); + effectiveFacing = mainFacingDir.GetRadian<32>(); + if (pType->Vector_AllowedTilt) + { + double dx = GetPos().X - pInvoker->GetCoords().X; + double dy = GetPos().Y - pInvoker->GetCoords().Y; + double dz = GetPos().Z - pInvoker->GetCoords().Z; + double lenXY = std::sqrt(dx * dx + dy * dy); + effectiveTilt = (lenXY > 1e-6) ? std::atan2(dz, lenXY) : 0.0; + } + } + break; + case VectorOrigin::Target: + { + CoordStruct targetPos; + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) + targetPos = pB->Target->GetCoords(); + else + break; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) + targetPos = pT->Target->GetCoords(); + else + break; + } + mainFacingDir = V_Point2Dir(targetPos, GetPos()); + effectiveFacing = mainFacingDir.GetRadian<32>(); + if (pType->Vector_AllowedTilt) + { + double dx = GetPos().X - targetPos.X; + double dy = GetPos().Y - targetPos.Y; + double dz = GetPos().Z - targetPos.Z; + double lenXY = std::sqrt(dx * dx + dy * dy); + effectiveTilt = (lenXY > 1e-6) ? std::atan2(dz, lenXY) : 0.0; + } + break; + } + case VectorOrigin::Self: + if (isBullet) + { + double vx = static_cast(pObject)->Velocity.X; + double vy = static_cast(pObject)->Velocity.Y; + effectiveFacing = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + mainFacingDir = V_Radians2Dir(effectiveFacing); + } + else + { + auto const pT = static_cast(pObject); + mainFacingDir = pType->Vector_OriginIsOnBody + ? pT->PrimaryFacing.Current() + : pT->TurretFacing(); + effectiveFacing = mainFacingDir.GetRadian<32>(); + } + break; + case VectorOrigin::Launcher: + if (pInvoker) + { + auto const pLT = static_cast(pInvoker); + mainFacingDir = pType->Vector_OriginIsOnBody + ? pLT->PrimaryFacing.Current() + : pLT->TurretFacing(); + effectiveFacing = mainFacingDir.GetRadian<32>(); + } + break; + } + } + + // === Origin coordinate (per-frame tracking) === + CoordStruct originPos = GetPos(); + + switch (pType->Vector_Origin) + { + case VectorOrigin::Target: + if (pType->Vector_OriginNoUpdate) + originPos = s.InitialOriginPos; + else if (isBullet) + { + auto const pB = static_cast(pObject); + originPos = pB->Target ? pB->Target->GetCoords() : pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + originPos = pT->Target ? pT->Target->GetCoords() : originPos; + } + break; + case VectorOrigin::Launcher: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Source: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Self: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : GetPos(); + break; + } + + if (pType->Vector_OriginFLH.isset() && pType->Vector_Origin != VectorOrigin::Self) + originPos = AttachEffectClass::GetFLHAbsoluteCoords(originPos, pType->Vector_OriginFLH, mainFacingDir); + + CoordStruct currentPos = GetPos(); + + // ================================================================ + // Circle mode + // ================================================================ + bool hasCircle = pType->Vector_CircleRadius > 0 || pType->Vector_CircleSpeed != 0 || pType->Vector_CircleAnglePerStep > 0.0 + || (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + || (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin); + + if (hasCircle) + { + double calcRadius = static_cast(pType->Vector_CircleRadius); + if (calcRadius <= 0.0) + { + double tdx = currentPos.X - originPos.X; + double tdy = currentPos.Y - originPos.Y; + calcRadius = std::sqrt(tdx * tdx + tdy * tdy); + } + + if (s.MovementFrames == 1) + { + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleSpeed); + s.CurrentCircleRadius = calcRadius; + if (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + s.CurrentCircleRadius = static_cast(rand() % (pType->Vector_CircleRandomRadiusMax - pType->Vector_CircleRandomRadiusMin + 1) + pType->Vector_CircleRandomRadiusMin); + } + s.CurrentCircleSpeed += pType->Vector_CircleSpeedAcceleration; + if (pType->Vector_CircleMaxSpeed != 0 && s.CurrentCircleSpeed > pType->Vector_CircleMaxSpeed) + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleMaxSpeed); + if (pType->Vector_CircleMinSpeed != 0 && s.CurrentCircleSpeed < pType->Vector_CircleMinSpeed) + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleMinSpeed); + + if (s.MovementFrames == 1) + { + s.CurrentCircleAngle = pType->Vector_CircleAnglePerStep; + if (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin) + s.CurrentCircleAngle = V_Random(pType->Vector_CircleRandomAngleMin, pType->Vector_CircleRandomAngleMax); + } + s.CurrentCircleAngle += pType->Vector_CircleAngleAcceleration; + if (pType->Vector_CircleMaxAngle != 0 && s.CurrentCircleAngle > pType->Vector_CircleMaxAngle) + s.CurrentCircleAngle = pType->Vector_CircleMaxAngle; + if (pType->Vector_CircleMinAngle != 0 && s.CurrentCircleAngle < pType->Vector_CircleMinAngle) + s.CurrentCircleAngle = pType->Vector_CircleMinAngle; + + double speed = s.CurrentCircleSpeed; + double angleStep = s.CurrentCircleAngle; + if (speed <= 0.0 && angleStep > 0.0) + speed = calcRadius * V_Deg2Rad(angleStep); + else if (angleStep <= 0.0 && speed > 0.0) + angleStep = V_Rad2Deg(speed / calcRadius); + + CoordStruct circleCenter = originPos; + if (pType->Vector_CircleOrigin.isset()) + { + if (pType->Vector_AllowOriginTilt) + circleCenter = AttachEffectClass::GetFLHAbsoluteCoords(originPos, pType->Vector_CircleOrigin, mainFacingDir); + else + circleCenter = { originPos.X + pType->Vector_CircleOrigin.Get().X, originPos.Y + pType->Vector_CircleOrigin.Get().Y, originPos.Z + pType->Vector_CircleOrigin.Get().Z }; + } + + bool hasOriginSub = pType->Vector_OriginMoveTo.isset() + || pType->Vector_OriginTargetFLH.isset() + || pType->Vector_OriginCircleRadius >= 0 || pType->Vector_OriginCircleSpeed != 0 || pType->Vector_OriginCircleAnglePerStep != 0; + + if (hasOriginSub) + { + CoordStruct baseCenter = originPos; + + if (pType->Vector_OriginOrigin != VectorOrigin::Self) + { + switch (pType->Vector_OriginOrigin) + { + case VectorOrigin::Launcher: + if (pInvoker) baseCenter = pInvoker->GetCoords(); + break; + case VectorOrigin::Target: + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) baseCenter = pB->Target->GetCoords(); + else if (pB->Owner && pB->Owner->Target) baseCenter = pB->Owner->Target->GetCoords(); + else baseCenter = pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) baseCenter = pT->Target->GetCoords(); + else + { + auto const pFoot = abstract_cast(pT); + if (pFoot && pFoot->Destination) baseCenter = pFoot->Destination->GetCoords(); + } + } + break; + case VectorOrigin::Source: + if (pInvoker) baseCenter = pInvoker->GetCoords(); + break; + } + } + else if (pType->Vector_OriginOriginFLH.isset()) + { + baseCenter.X += pType->Vector_OriginOriginFLH.Get().X; + baseCenter.Y += pType->Vector_OriginOriginFLH.Get().Y; + baseCenter.Z += pType->Vector_OriginOriginFLH.Get().Z; + } + + if (s.MovementFrames == 1) + { + s.OriginOffset = { circleCenter.X - baseCenter.X, circleCenter.Y - baseCenter.Y, circleCenter.Z - baseCenter.Z }; + s.OriginCircleRadiusRuntime = static_cast(pType->Vector_OriginCircleRadius); + s.OriginCircleSpeedRuntime = static_cast(pType->Vector_OriginCircleSpeed); + s.OriginCircleAngleRuntime = 0.0; + s.OriginTargetOffset = CoordStruct::Empty; + + if (pType->Vector_OriginNormalVector.isset()) + { + CoordStruct nv = pType->Vector_OriginNormalVector.Get(); + double len = std::sqrt(static_cast(nv.X * nv.X + nv.Y * nv.Y)); + s.OriginFacing = len > 1e-6 ? std::atan2(static_cast(nv.Y), static_cast(nv.X)) : 0; + s.OriginTilt = len > 1e-6 ? std::atan2(static_cast(nv.Z), len) : (nv.Z > 0 ? 3.14159265358979323846 / 2.0 : -3.14159265358979323846 / 2.0); + } + } + + s.OriginNormalRotFRuntime += s.OriginNormalStepF; + s.OriginNormalRotLRuntime += s.OriginNormalStepL; + s.OriginNormalRotHRuntime += s.OriginNormalStepH; + + double oFacing = s.OriginFacing + V_Deg2Rad(s.OriginNormalRotHRuntime); + double oTilt = s.OriginTilt + V_Deg2Rad(s.OriginNormalRotLRuntime); + + CoordStruct originCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + CoordStruct disp{ 0, 0, 0 }; + + if (pType->Vector_OriginMoveTo.isset()) + { + s.OriginAngle += pType->Vector_OriginCircleAnglePerStep; + CoordStruct mto = pType->Vector_OriginMoveTo.Get(); + if (pType->Vector_OriginGrowRate.isset()) + { + mto.X += pType->Vector_OriginGrowRate.Get().X * s.OriginElapsed; + mto.Y += pType->Vector_OriginGrowRate.Get().Y * s.OriginElapsed; + mto.Z += pType->Vector_OriginGrowRate.Get().Z * s.OriginElapsed; + } + disp = V_FLHAbsoluteOffset(mto, V_Radians2Dir(oFacing + V_Deg2Rad(s.OriginAngle))); + } + else if (pType->Vector_OriginTargetFLH.isset()) + { + if (s.OriginElapsed == 0) + s.OriginSpeed = pType->Vector_OriginInitialSpeed >= 0 ? pType->Vector_OriginInitialSpeed : 40.0; + + CoordStruct targetWorld = AttachEffectClass::GetFLHAbsoluteCoords(baseCenter, pType->Vector_OriginTargetFLH.Get() + s.OriginTargetOffset, V_Radians2Dir(oFacing)); + int dx = targetWorld.X - originCenter.X, dy = targetWorld.Y - originCenter.Y, dz = targetWorld.Z - originCenter.Z; + double dist = std::sqrt(static_cast(dx * dx + dy * dy + dz * dz)); + if (dist >= 1.0) + { + double sv = s.OriginSpeed / dist; + disp = { static_cast(dx * sv), static_cast(dy * sv), static_cast(dz * sv) }; + } + } + else + { + s.OriginCircleRadiusRuntime += pType->Vector_OriginCircleRadiusGrow; + double tr = s.OriginCircleRadiusRuntime; + if (pType->Vector_OriginCircleMaxRadius > 0 && tr > pType->Vector_OriginCircleMaxRadius) tr = pType->Vector_OriginCircleMaxRadius; + if (pType->Vector_OriginCircleMinRadius > 0 && tr < pType->Vector_OriginCircleMinRadius) tr = pType->Vector_OriginCircleMinRadius; + double stepO = pType->Vector_OriginCircleAnglePerStep; + if (pType->Vector_OriginCircleSpeed != 0 && tr > 0) + stepO = V_Rad2Deg(pType->Vector_OriginCircleSpeed / tr); + s.OriginCircleAngleRuntime += stepO; + double r = V_Deg2Rad(stepO); + double ca = std::cos(r), sa = std::sin(r); + double dxO = static_cast(s.OriginOffset.X); + double dyO = static_cast(s.OriginOffset.Y); + double dzO = static_cast(s.OriginOffset.Z); + double cf = std::cos(oFacing), sf = std::sin(oFacing), ct = std::cos(oTilt), st = std::sin(oTilt); + double dL = dxO * (-sf) + dyO * cf; + double dH = dxO * (-cf * st) + dyO * (-sf * st) + dzO * ct; + double cd = std::sqrt(dL * dL + dH * dH); + if (cd < 1.0 && tr > 0) { dL = tr; dH = 0; cd = tr; } + else if (cd < 1.0) cd = 1.0; + double rL = (dL / cd * tr * ca) - (dH / cd * tr * sa); + double rH = (dL / cd * tr * sa) + (dH / cd * tr * ca); + CoordStruct newOffset; + newOffset.X = static_cast(rL * (-sf) + rH * (-cf * st)); + newOffset.Y = static_cast(rL * cf + rH * (-sf * st)); + newOffset.Z = static_cast(rH * ct); + disp.X = newOffset.X - s.OriginOffset.X; + disp.Y = newOffset.Y - s.OriginOffset.Y; + disp.Z = newOffset.Z - s.OriginOffset.Z; + } + + s.OriginOffset.X += disp.X; s.OriginOffset.Y += disp.Y; s.OriginOffset.Z += disp.Z; + circleCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + s.OriginElapsed++; + } + + CoordStruct centerDelta{ 0, 0, 0 }; + bool useCenterTracking = false; + if (s.PrevCircleCenter.X || s.PrevCircleCenter.Y || s.PrevCircleCenter.Z) + { + centerDelta.X = circleCenter.X - s.PrevCircleCenter.X; + centerDelta.Y = circleCenter.Y - s.PrevCircleCenter.Y; + centerDelta.Z = circleCenter.Z - s.PrevCircleCenter.Z; + useCenterTracking = (pType->Vector_OriginCircleRadius >= 0 || pType->Vector_OriginCircleSpeed != 0 || pType->Vector_OriginCircleAnglePerStep != 0); + } + s.PrevCircleCenter = circleCenter; + + CoordStruct trackPos = currentPos; + if (useCenterTracking) + { + trackPos.X += centerDelta.X; trackPos.Y += centerDelta.Y; trackPos.Z += centerDelta.Z; + } + + double dx = static_cast(trackPos.X - circleCenter.X); + double dy = static_cast(trackPos.Y - circleCenter.Y); + double dz = static_cast(trackPos.Z - circleCenter.Z); + double currentDist; + bool useTiltPlane = hasNormal || (pType->Vector_AllowedTilt && effectiveTilt != 0.0); + + if (useTiltPlane) + { + double cosF = std::cos(effectiveFacing), sinF = std::sin(effectiveFacing); + double cosT = std::cos(effectiveTilt), sinT = std::sin(effectiveTilt); + double dL = dx * (-sinF) + dy * cosF; + double dH = dx * (-cosF * sinT) + dy * (-sinF * sinT) + dz * cosT; + currentDist = std::sqrt(dL * dL + dH * dH); + } + else + { + currentDist = std::sqrt(dx * dx + dy * dy); + } + bool startAtCenter = currentDist < 1.0; + if (currentDist < 1.0) currentDist = 1.0; + + s.CurrentCircleRadius += pType->Vector_CircleRadiusGrow; + double targetRadius = s.CurrentCircleRadius; + if (pType->Vector_CircleMaxRadius > 0 && targetRadius > pType->Vector_CircleMaxRadius) + targetRadius = static_cast(pType->Vector_CircleMaxRadius); + if (pType->Vector_CircleMinRadius > 0 && targetRadius < pType->Vector_CircleMinRadius) + targetRadius = static_cast(pType->Vector_CircleMinRadius); + + if (startAtCenter) + { + dx = targetRadius; dy = 0.0; currentDist = targetRadius; + } + + double rad = V_Deg2Rad(angleStep); + double cosA = std::cos(rad), sinA = std::sin(rad); + CoordStruct moveDisp{ 0, 0, 0 }; + + if (useTiltPlane) + { + double cosF = std::cos(effectiveFacing), sinF = std::sin(effectiveFacing); + double cosT = std::cos(effectiveTilt), sinT = std::sin(effectiveTilt); + double dL = dx * (-sinF) + dy * cosF; + double dH = dx * (-cosF * sinT) + dy * (-sinF * sinT) + dz * cosT; + double curDist = std::sqrt(dL * dL + dH * dH); + if (curDist < 1.0) curDist = 1.0; + double ndL = dL / curDist * targetRadius; + double ndH = dH / curDist * targetRadius; + double rL = ndL * cosA - ndH * sinA; + double rH = ndL * sinA + ndH * cosA; + moveDisp.X = circleCenter.X + static_cast(rL * (-sinF) + rH * (-cosF * sinT)) - currentPos.X; + moveDisp.Y = circleCenter.Y + static_cast(rL * cosF + rH * (-sinF * sinT)) - currentPos.Y; + moveDisp.Z = circleCenter.Z + static_cast(rH * cosT) - currentPos.Z; + } + else + { + double ndx = dx / currentDist * targetRadius; + double ndy = dy / currentDist * targetRadius; + double rx = ndx * cosA - ndy * sinA; + double ry = ndx * sinA + ndy * cosA; + moveDisp.X = circleCenter.X + static_cast(rx) - currentPos.X; + moveDisp.Y = circleCenter.Y + static_cast(ry) - currentPos.Y; + moveDisp.Z = circleCenter.Z - currentPos.Z; + } + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = moveDisp.X; + pB->Velocity.Y = moveDisp.Y; + pB->Velocity.Z = moveDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + + if (pType->Vector_CircleEndOnMaxRadius && pType->Vector_CircleMaxRadius > 0 && s.CurrentCircleRadius >= pType->Vector_CircleMaxRadius) + return; + if (pType->Vector_CircleEndOnMinRadius && pType->Vector_CircleMinRadius > 0 && s.CurrentCircleRadius <= pType->Vector_CircleMinRadius) + return; + + return; +} + +// ================================================================ +// MoveTo mode +// ================================================================ + if (static_cast(pType->Vector_MoveTo) != CoordStruct::Empty) + { + DirStruct moveDir = mainFacingDir; + if (pType->Vector_AnglePerStep != 0.0) + { + if (s.MovementFrames == 1) + s.CurrentAngle = 0.0; + s.CurrentAngle += pType->Vector_AnglePerStep; + moveDir = V_Radians2Dir(mainFacingDir.GetRadian<32>() + V_Deg2Rad(s.CurrentAngle)); + } + + CoordStruct grow{ static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().X * s.MovementFrames : 0), + static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().Y * s.MovementFrames : 0), + static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().Z * s.MovementFrames : 0) }; + CoordStruct mt = pType->Vector_MoveTo; + CoordStruct moveFlh = { mt.X + grow.X, mt.Y + grow.Y, mt.Z + grow.Z }; + + CoordStruct moveDisp = V_FLHAbsoluteOffset(moveFlh, moveDir); + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = moveDisp.X; pB->Velocity.Y = moveDisp.Y; pB->Velocity.Z = moveDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + return; + } + + // ================================================================ + // TargetFLH modes: ReachTarget / Speed + // ================================================================ + if (!pType->Vector_TargetFLH.isset()) + { + if (!isBullet && pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + return; + } + + CoordStruct frameTargetFlh = { pType->Vector_TargetFLH.Get().X + s.TargetOffset.X, + pType->Vector_TargetFLH.Get().Y + s.TargetOffset.Y, + pType->Vector_TargetFLH.Get().Z + s.TargetOffset.Z }; + CoordStruct frameTarget; + + switch (pType->Vector_Origin) + { + case VectorOrigin::Launcher: + if (pInvoker) + frameTarget = TechnoExt::GetFLHAbsoluteCoords(static_cast(pInvoker), frameTargetFlh, !pType->Vector_OriginIsOnBody); + else + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, mainFacingDir); + break; + case VectorOrigin::Self: + if (pType->Vector_OriginIsOnWorld) + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, DirStruct{}); + else if (isBullet) + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(currentPos, frameTargetFlh, mainFacingDir); + else + frameTarget = TechnoExt::GetFLHAbsoluteCoords(static_cast(pObject), frameTargetFlh, !pType->Vector_OriginIsOnBody); + break; + default: + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, mainFacingDir); + break; + } + + CoordStruct dirVec = { frameTarget.X - currentPos.X, frameTarget.Y - currentPos.Y, frameTarget.Z - currentPos.Z }; + double dirLen = std::sqrt(static_cast(dirVec.X * dirVec.X + dirVec.Y * dirVec.Y + dirVec.Z * dirVec.Z)); + CoordStruct resultDisp{ 0, 0, 0 }; + + if (pType->Vector_ReachTarget) + { + int totalDuration = pType->Duration / pType->Vector_TimeStep; + if (totalDuration < 1) totalDuration = 1; + int effectiveDuration = totalDuration - pType->Vector_DisabledFrames; + if (effectiveDuration < 1) effectiveDuration = 1; + int remainingFrames = effectiveDuration - s.MovementFrames + 1; + + if (pType->Vector_ReachTargetEarlyEnd > 0 && pType->Vector_ReachTargetEarlyEnd < effectiveDuration + && remainingFrames <= pType->Vector_ReachTargetEarlyEnd) + { + s.DisabledTimer = pType->Vector_ReachTargetEarlyEnd; + return; + } + + if (remainingFrames <= 0) + { + if (pType->Vector_Force) + { + resultDisp.X = frameTarget.X - currentPos.X; + resultDisp.Y = frameTarget.Y - currentPos.Y; + resultDisp.Z = frameTarget.Z - currentPos.Z; + } + } + else if (dirLen > 1e-6) + { + double adjustedSpeed = dirLen / remainingFrames; + resultDisp.X = static_cast(dirVec.X / dirLen * adjustedSpeed); + resultDisp.Y = static_cast(dirVec.Y / dirLen * adjustedSpeed); + resultDisp.Z = static_cast(dirVec.Z / dirLen * adjustedSpeed); + + if (s.ArcHeight != 0) + { + double t = static_cast(s.MovementFrames - 1) / effectiveDuration; + double tNext = static_cast(s.MovementFrames) / effectiveDuration; + double peakScale = pType->Vector_ArcPeakPercent > 0 ? 1.0 / (pType->Vector_ArcPeakPercent * (1.0 - pType->Vector_ArcPeakPercent)) : 4.0; + double delta = peakScale * s.ArcHeight * (tNext * (1.0 - tNext) - t * (1.0 - t)); + + if (s.ArcRotation == 0.0) + { + resultDisp.Z += static_cast(delta); + } + else + { + double ax = frameTarget.X - s.InitialLocation.X; + double ay = frameTarget.Y - s.InitialLocation.Y; + double az = frameTarget.Z - s.InitialLocation.Z; + double aLen = std::sqrt(ax * ax + ay * ay + az * az); + if (aLen > 1e-6) + { + double dnx = ax / aLen, dny = ay / aLen, dnz = az / aLen; + double upDotD = dnz; + double px = -dnx * upDotD, py = -dny * upDotD, pz = 1.0 - dnz * upDotD; + double pLen = std::sqrt(px * px + py * py + pz * pz); + if (pLen < 1e-6) { px = 1.0 - dnx * dnx; py = -dny * dnx; pz = -dnz * dnx; pLen = std::sqrt(px * px + py * py + pz * pz); } + double pnx = px / pLen, pny = py / pLen, pnz = pz / pLen; + double arcRad = V_Deg2Rad(s.ArcRotation); + double c = std::cos(arcRad), s_arc = std::sin(arcRad); + double rx = pnx * c + (dny * pnz - dnz * pny) * s_arc; + double ry = pny * c + (dnz * pnx - dnx * pnz) * s_arc; + double rz = pnz * c + (dnx * pny - dny * pnx) * s_arc; + resultDisp.X += static_cast(rx * delta); + resultDisp.Y += static_cast(ry * delta); + resultDisp.Z += static_cast(rz * delta); + } + else + { + resultDisp.Z += static_cast(delta); + } + } + } + } + } + else if (dirLen > 1e-6) + { + double spd = s.CurrentSpeed; + if (pType->Vector_Acceleration != 0) spd += pType->Vector_Acceleration * s.CurrentFrame; + if (pType->Vector_MinSpeed >= 0 && spd < pType->Vector_MinSpeed) spd = pType->Vector_MinSpeed; + if (pType->Vector_MaxSpeed >= 0 && spd > pType->Vector_MaxSpeed) spd = pType->Vector_MaxSpeed; + resultDisp.X = static_cast(dirVec.X / dirLen * spd); + resultDisp.Y = static_cast(dirVec.Y / dirLen * spd); + resultDisp.Z = static_cast(dirVec.Z / dirLen * spd); + } + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = resultDisp.X; + pB->Velocity.Y = resultDisp.Y; + pB->Velocity.Z = resultDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + resultDisp.X, currentPos.Y + resultDisp.Y, currentPos.Z + resultDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + + if (!isBullet && pType->Vector_SyncFacing && (resultDisp.X != 0 || resultDisp.Y != 0)) + { + double ang = std::atan2(static_cast(resultDisp.Y), static_cast(resultDisp.X)); + auto dir = DirStruct(static_cast(ang * 32768.0 / 3.14159265358979323846)); + static_cast(pObject)->PrimaryFacing.SetDesired(dir); + } + + if (pType->Vector_AllowFallingDestroy && pType->Vector_FallingDestroyHeight > 0) + { + if (s.InitialLocation.Z - GetPos().Z > pType->Vector_FallingDestroyHeight) + return; + } +} From ad9bf491e85c0d988a088a79305e2a429da79c30 Mon Sep 17 00:00:00 2001 From: KenosisM Date: Sat, 27 Jun 2026 13:47:38 +0800 Subject: [PATCH 11/11] feat: default Vector.AffectTechno=false, Vector.AffectBullets=true --- src/New/Type/AttachEffectTypeClass.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/New/Type/AttachEffectTypeClass.h b/src/New/Type/AttachEffectTypeClass.h index bf1dfead11..58d02f8782 100644 --- a/src/New/Type/AttachEffectTypeClass.h +++ b/src/New/Type/AttachEffectTypeClass.h @@ -366,8 +366,8 @@ class AttachEffectTypeClass final : public Enumerable , Vector_Acceleration { 0 } , Vector_AllowFallingDestroy { false } , Vector_FallingDestroyHeight { 0 } - , Vector_AffectTechno { true } - , Vector_AffectBullets { false } + , Vector_AffectTechno { false } + , Vector_AffectBullets { true } , Vector_CircleRandomRadiusMin { -1 } , Vector_CircleRandomRadiusMax { -1 } , Vector_CircleRandomAngleMin { -1 }