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/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/VectorPort.rar b/VectorPort.rar
new file mode 100644
index 0000000000..d07db5c96d
Binary files /dev/null and b/VectorPort.rar differ
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