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(pObject)->PrimaryFacing.SetDesired(dir); + } + + if (pType->Vector_AllowFallingDestroy && pType->Vector_FallingDestroyHeight > 0) + { + if (s.InitialLocation.Z - GetPos().Z > pType->Vector_FallingDestroyHeight) + return; + } +} diff --git a/VectorPort/src/New/Entity/VectorState.h b/VectorPort/src/New/Entity/VectorState.h new file mode 100644 index 0000000000..cc08bed4fd --- /dev/null +++ b/VectorPort/src/New/Entity/VectorState.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +#include + +struct VectorState +{ + bool Initialized; + int CurrentFrame; + int DisabledTimer; + double CurrentSpeed; + double CurrentAngle; + double CurrentCircleRadius; + double CurrentCircleSpeed; + double CurrentCircleAngle; + CoordStruct InitialOriginPos; + CoordStruct InitialLocation; + CoordStruct PrevCirclePos; + double ArcHeight; + double ArcRotation; + CoordStruct TargetOffset; + double NormalRotF; + double NormalRotL; + double NormalRotH; + double NormalStepF; + double NormalStepL; + double NormalStepH; + int MovementFrames; + double FacingRad; + double TiltRad; + CoordStruct OriginOffset; + CoordStruct PrevCircleCenter; + int OriginElapsed; + double OriginSpeed; + double OriginAngle; + CoordStruct OriginTargetOffset; + double OriginCircleRadiusRuntime; + double OriginCircleSpeedRuntime; + double OriginCircleAngleRuntime; + double OriginNormalRotFRuntime; + double OriginNormalRotLRuntime; + double OriginNormalRotHRuntime; + double OriginNormalStepF; + double OriginNormalStepL; + double OriginNormalStepH; + double OriginFacing; + double OriginTilt; + + VectorState() + : Initialized { false } + , CurrentFrame { 0 } + , DisabledTimer { 0 } + , CurrentSpeed { 0.0 } + , CurrentAngle { 0.0 } + , CurrentCircleRadius { 0.0 } + , CurrentCircleSpeed { 0.0 } + , CurrentCircleAngle { 0.0 } + , InitialOriginPos { CoordStruct::Empty } + , InitialLocation { CoordStruct::Empty } + , PrevCirclePos { CoordStruct::Empty } + , ArcHeight { 0.0 } + , ArcRotation { 0.0 } + , TargetOffset { CoordStruct::Empty } + , NormalRotF { 0.0 } + , NormalRotL { 0.0 } + , NormalRotH { 0.0 } + , NormalStepF { 0.0 } + , NormalStepL { 0.0 } + , NormalStepH { 0.0 } + , MovementFrames { 0 } + , FacingRad { 0.0 } + , TiltRad { 0.0 } + , OriginOffset { CoordStruct::Empty } + , PrevCircleCenter { CoordStruct::Empty } + , OriginElapsed { 0 } + , OriginSpeed { 0.0 } + , OriginAngle { 0.0 } + , OriginTargetOffset { CoordStruct::Empty } + , OriginCircleRadiusRuntime { 0.0 } + , OriginCircleSpeedRuntime { 0.0 } + , OriginCircleAngleRuntime { 0.0 } + , OriginNormalRotFRuntime { 0.0 } + , OriginNormalRotLRuntime { 0.0 } + , OriginNormalRotHRuntime { 0.0 } + , OriginNormalStepF { 0.0 } + , OriginNormalStepL { 0.0 } + , OriginNormalStepH { 0.0 } + , OriginFacing { 0.0 } + , OriginTilt { 0.0 } + { } +}; + +void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorState& state, ObjectClass* pInvoker, bool isBullet); diff --git a/VectorPort/src/New/Type/AttachEffectTypeClass.cpp b/VectorPort/src/New/Type/AttachEffectTypeClass.cpp new file mode 100644 index 0000000000..76b7d8113a --- /dev/null +++ b/VectorPort/src/New/Type/AttachEffectTypeClass.cpp @@ -0,0 +1,716 @@ +#include "AttachEffectTypeClass.h" + +#include + +// Used to match groups names to AttachEffectTypeClass instances. Do not iterate due to undetermined order being prone to desyncs. +std::unordered_map> AttachEffectTypeClass::GroupsMap; + +bool AttachEffectTypeClass::HasGroup(const std::string& groupID) const +{ + for (auto const& group : this->Groups) + { + if (!group.compare(groupID)) + return true; + } + + return false; +} + +bool AttachEffectTypeClass::HasGroups(const std::vector& groupIDs, bool requireAll) const +{ + size_t foundCount = 0; + + for (auto const& group : this->Groups) + { + for (auto const& requiredGroup : groupIDs) + { + if (!group.compare(requiredGroup)) + { + if (!requireAll) + return true; + + foundCount++; + } + } + } + + return !requireAll ? false : foundCount >= groupIDs.size(); +} + +std::vector AttachEffectTypeClass::GetTypesFromGroups(const std::vector& groupIDs) +{ + std::set types; + auto const map = &AttachEffectTypeClass::GroupsMap; + + for (auto const& group : groupIDs) + { + if (map->contains(group)) + { + auto const values = &map->at(group); + types.insert(values->begin(), values->end()); + } + } + + return std::vector(types.begin(), types.end()); +} + +void AttachEffectTypeClass::HandleEvent(TechnoClass* pTarget) +{ + if (const auto pTag = pTarget->AttachedTag) + pTag->RaiseEvent((TriggerEvent)PhobosTriggerEvent::AttachedIsUnderAttachedEffect, pTarget, CellStruct::Empty); +} + +template<> +const char* Enumerable::GetMainSection() +{ + return "AttachEffectTypes"; +} + +void AttachEffectTypeClass::AddToGroupsMap() +{ + auto const map = &AttachEffectTypeClass::GroupsMap; + + for (auto const& group : this->Groups) + { + if (!map->contains(group)) + { + map->insert(std::make_pair(group, std::set{this})); + } + else + { + auto const values = &map->at(group); + values->insert(this); + } + } +} + +void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) +{ + const char* pSection = this->Name; + + if (INIClass::IsBlank(pSection) || !pINI->GetSection(pSection)) + return; + + INI_EX exINI(pINI); + + this->Duration.Read(exINI, pSection, "Duration"); + this->Duration_ApplyFirepowerMult.Read(exINI, pSection, "Duration.ApplyFirepowerMult"); + this->Duration_ApplyArmorMultOnTarget.Read(exINI, pSection, "Duration.ApplyArmorMultOnTarget"); + this->Cumulative.Read(exINI, pSection, "Cumulative"); + this->Cumulative_MaxCount.Read(exINI, pSection, "Cumulative.MaxCount"); + this->Powered.Read(exINI, pSection, "Powered"); + this->DiscardOn.Read(exINI, pSection, "DiscardOn"); + this->DiscardOn_RangeOverride.Read(exINI, pSection, "DiscardOn.RangeOverride"); + this->DiscardOn_MoveBasedOnDestination.Read(exINI, pSection, "DiscardOn.MoveBasedOnDestination"); + this->PenetratesIronCurtain.Read(exINI, pSection, "PenetratesIronCurtain"); + this->PenetratesForceShield.Read(exINI, pSection, "PenetratesForceShield"); + this->AffectTypes.Read(exINI, pSection, "AffectTypes"); + this->IgnoreTypes.Read(exINI, pSection, "IgnoreTypes"); + if (exINI.ReadString(pSection, "AffectTargets") > 0) + { + Debug::Log("[Developer warning][%s] AffectTargets is deprecated and has been replaced by AffectsTarget! If both are set, the latter will be used.\n", pSection); + } + this->AffectsTarget.Read(exINI, pSection, "AffectTargets"); // Temporary solution for the INI tags renaming issue, see #2093 + this->AffectsTarget.Read(exINI, pSection, "AffectsTarget"); + + this->Animation.Read(exINI, pSection, "Animation"); + this->CumulativeAnimations.Read(exINI, pSection, "CumulativeAnimations"); + this->CumulativeAnimations_RestartOnChange.Read(exINI, pSection, "CumulativeAnimations.RestartOnChange"); + this->Animation_ResetOnReapply.Read(exINI, pSection, "Animation.ResetOnReapply"); + this->Animation_OfflineAction.Read(exINI, pSection, "Animation.OfflineAction"); + this->Animation_TemporalAction.Read(exINI, pSection, "Animation.TemporalAction"); + this->Animation_UseInvokerAsOwner.Read(exINI, pSection, "Animation.UseInvokerAsOwner"); + this->Animation_HideIfAttachedWith.Read(exINI, pSection, "Animation.HideIfAttachedWith"); + + this->ExpireWeapon.Read(exINI, pSection, "ExpireWeapon"); + this->ExpireWeapon_TriggerOn.Read(exINI, pSection, "ExpireWeapon.TriggerOn"); + this->ExpireWeapon_CumulativeOnlyOnce.Read(exINI, pSection, "ExpireWeapon.CumulativeOnlyOnce"); + this->ExpireWeapon_UseInvokerAsOwner.Read(exINI, pSection, "ExpireWeapon.UseInvokerAsOwner"); + this->Next.Read(exINI, pSection, "Next"); + + this->Tint_Color.Read(exINI, pSection, "Tint.Color"); + this->Tint_Intensity.Read(exINI, pSection, "Tint.Intensity"); + this->Tint_VisibleToHouses.Read(exINI, pSection, "Tint.VisibleToHouses"); + + this->FirepowerMultiplier.Read(exINI, pSection, "FirepowerMultiplier"); + this->ArmorMultiplier.Read(exINI, pSection, "ArmorMultiplier"); + this->ArmorMultiplier_AllowWarheads.Read(exINI, pSection, "ArmorMultiplier.AllowWarheads"); + this->ArmorMultiplier_DisallowWarheads.Read(exINI, pSection, "ArmorMultiplier.DisallowWarheads"); + this->SpeedMultiplier.Read(exINI, pSection, "SpeedMultiplier"); + this->ROFMultiplier.Read(exINI, pSection, "ROFMultiplier"); + this->ROFMultiplier_ApplyOnCurrentTimer.Read(exINI, pSection, "ROFMultiplier.ApplyOnCurrentTimer"); + + this->Cloakable.Read(exINI, pSection, "Cloakable"); + this->ForceDecloak.Read(exINI, pSection, "ForceDecloak"); + + this->WeaponRange_Multiplier.Read(exINI, pSection, "WeaponRange.Multiplier"); + this->WeaponRange_ExtraRange.Read(exINI, pSection, "WeaponRange.ExtraRange"); + this->WeaponRange_AllowWeapons.Read(exINI, pSection, "WeaponRange.AllowWeapons"); + this->WeaponRange_DisallowWeapons.Read(exINI, pSection, "WeaponRange.DisallowWeapons"); + + this->Crit_Multiplier.Read(exINI, pSection, "Crit.Multiplier"); + this->Crit_ExtraChance.Read(exINI, pSection, "Crit.ExtraChance"); + this->Crit_AllowWarheads.Read(exINI, pSection, "Crit.AllowWarheads"); + this->Crit_DisallowWarheads.Read(exINI, pSection, "Crit.DisallowWarheads"); + + this->RevengeWeapon.Read(exINI, pSection, "RevengeWeapon"); + if (exINI.ReadString(pSection, "RevengeWeapon.AffectsHouses") > 0) + { + Debug::Log("[Developer warning][%s] RevengeWeapon.AffectsHouses is deprecated and has been replaced by RevengeWeapon.AffectsHouse! If both are set, the latter will be used.\n", pSection); + } + this->RevengeWeapon_AffectsHouse.Read(exINI, pSection, "RevengeWeapon.AffectsHouses"); // Temporary solution for the INI tags renaming issue, see #2093 + this->RevengeWeapon_AffectsHouse.Read(exINI, pSection, "RevengeWeapon.AffectsHouse"); + this->RevengeWeapon_UseInvokerAsOwner.Read(exINI, pSection, "RevengeWeapon.UseInvokerAsOwner"); + + this->ReflectDamage.Read(exINI, pSection, "ReflectDamage"); + this->ReflectDamage_Warhead.Read(exINI, pSection, "ReflectDamage.Warhead"); + this->ReflectDamage_Warhead_Detonate.Read(exINI, pSection, "ReflectDamage.Warhead.Detonate"); + this->ReflectDamage_Multiplier.Read(exINI, pSection, "ReflectDamage.Multiplier"); + if (exINI.ReadString(pSection, "ReflectDamage.AffectsHouses") > 0) + { + Debug::Log("[Developer warning][%s] ReflectDamage.AffectsHouses is deprecated and has been replaced by ReflectDamage.AffectsHouse! If both are set, the latter will be used.\n", pSection); + } + this->ReflectDamage_AffectsHouse.Read(exINI, pSection, "ReflectDamage.AffectsHouses"); // Temporary solution for the INI tags renaming issue, see #2093 + this->ReflectDamage_AffectsHouse.Read(exINI, pSection, "ReflectDamage.AffectsHouse"); + this->ReflectDamage_Chance.Read(exINI, pSection, "ReflectDamage.Chance"); + this->ReflectDamage_Override.Read(exINI, pSection, "ReflectDamage.Override"); + this->ReflectDamage_UseInvokerAsOwner.Read(exINI, pSection, "ReflectDamage.UseInvokerAsOwner"); + + this->DisableWeapons.Read(exINI, pSection, "DisableWeapons"); + this->Unkillable.Read(exINI, pSection, "Unkillable"); + this->LaserTrail_Type.Read(exINI, pSection, "LaserTrail.Type"); + + // Vector + this->Vector_TimeStep.Read(exINI, pSection, "Vector.TimeStep"); + if (this->Vector_TimeStep < 1) + this->Vector_TimeStep = 1; + this->Vector_DisabledFrames.Read(exINI, pSection, "Vector.DisabledFrames"); + this->Vector_SyncFacing.Read(exINI, pSection, "Vector.SyncFacing"); + this->Vector_OriginIsOnWorld.Read(exINI, pSection, "Vector.OriginIsOnWorld"); + this->Vector_OriginIsOnBody.Read(exINI, pSection, "Vector.OriginIsOnBody"); + this->Vector_Origin.Read(exINI, pSection, "Vector.Origin"); + this->Vector_OriginFLH.Read(exINI, pSection, "Vector.OriginFLH"); + this->Vector_OriginNoUpdate.Read(exINI, pSection, "Vector.OriginNoUpdate"); + this->Vector_Force.Read(exINI, pSection, "Vector.Force"); + this->Vector_Freeze.Read(exINI, pSection, "Vector.Freeze"); + this->Vector_AllowedTilt.Read(exINI, pSection, "Vector.AllowedTilt"); + this->Vector_NormalVector.Read(exINI, pSection, "Vector.NormalVector"); + this->Vector_NormalRandomF.Read(exINI, pSection, "Vector.NormalRandomF"); + this->Vector_NormalRandomL.Read(exINI, pSection, "Vector.NormalRandomL"); + this->Vector_NormalRandomH.Read(exINI, pSection, "Vector.NormalRandomH"); + this->Vector_NormalFAnglePerStep.Read(exINI, pSection, "Vector.NormalFAnglePerStep"); + this->Vector_NormalLAnglePerStep.Read(exINI, pSection, "Vector.NormalLAnglePerStep"); + this->Vector_NormalHAnglePerStep.Read(exINI, pSection, "Vector.NormalHAnglePerStep"); + this->Vector_MoveTo.Read(exINI, pSection, "Vector.MoveTo"); + this->Vector_GrowRate.Read(exINI, pSection, "Vector.GrowRate"); + this->Vector_AnglePerStep.Read(exINI, pSection, "Vector.AnglePerStep"); + this->Vector_CircleRadius.Read(exINI, pSection, "Vector.CircleRadius"); + this->Vector_CircleSpeed.Read(exINI, pSection, "Vector.CircleSpeed"); + this->Vector_CircleSpeedAcceleration.Read(exINI, pSection, "Vector.CircleSpeedAcceleration"); + this->Vector_CircleMaxSpeed.Read(exINI, pSection, "Vector.CircleMaxSpeed"); + this->Vector_CircleMinSpeed.Read(exINI, pSection, "Vector.CircleMinSpeed"); + this->Vector_CircleAnglePerStep.Read(exINI, pSection, "Vector.CircleAnglePerStep"); + this->Vector_CircleAngleAcceleration.Read(exINI, pSection, "Vector.CircleAngleAcceleration"); + this->Vector_CircleOrigin.Read(exINI, pSection, "Vector.CircleOrigin"); + this->Vector_AllowOriginTilt.Read(exINI, pSection, "Vector.AllowOriginTilt"); + this->Vector_CircleRadiusGrow.Read(exINI, pSection, "Vector.CircleRadiusGrow"); + this->Vector_CircleMaxRadius.Read(exINI, pSection, "Vector.CircleMaxRadius"); + this->Vector_CircleMinRadius.Read(exINI, pSection, "Vector.CircleMinRadius"); + this->Vector_CircleEndOnMaxRadius.Read(exINI, pSection, "Vector.CircleEndOnMaxRadius"); + this->Vector_CircleEndOnMinRadius.Read(exINI, pSection, "Vector.CircleEndOnMinRadius"); + this->Vector_TargetFLH.Read(exINI, pSection, "Vector.TargetFLH"); + this->Vector_ReachTarget.Read(exINI, pSection, "Vector.ReachTarget"); + this->Vector_ReachTargetEarlyEnd.Read(exINI, pSection, "Vector.ReachTargetEarlyEnd"); + this->Vector_ArcHeight.Read(exINI, pSection, "Vector.ArcHeight"); + this->Vector_ArcPeakPercent.Read(exINI, pSection, "Vector.ArcPeakPercent"); + this->Vector_ArcPeakRandomPercent.Read(exINI, pSection, "Vector.ArcPeakRandomPercent"); + this->Vector_ArcRotation.Read(exINI, pSection, "Vector.ArcRotation"); + this->Vector_InitialSpeed.Read(exINI, pSection, "Vector.InitialSpeed"); + this->Vector_MaxSpeed.Read(exINI, pSection, "Vector.MaxSpeed"); + this->Vector_MinSpeed.Read(exINI, pSection, "Vector.MinSpeed"); + this->Vector_Acceleration.Read(exINI, pSection, "Vector.Acceleration"); + this->Vector_AllowFallingDestroy.Read(exINI, pSection, "Vector.AllowFallingDestroy"); + this->Vector_FallingDestroyHeight.Read(exINI, pSection, "Vector.FallingDestroyHeight"); + this->Vector_AffectTechno.Read(exINI, pSection, "Vector.AffectTechno"); + this->Vector_AffectBullets.Read(exINI, pSection, "Vector.AffectBullets"); + this->Vector_CircleRandomRadiusMin.Read(exINI, pSection, "Vector.CircleRandomRadius.Min"); + this->Vector_CircleRandomRadiusMax.Read(exINI, pSection, "Vector.CircleRandomRadius.Max"); + this->Vector_CircleRandomAngleMin.Read(exINI, pSection, "Vector.CircleRandomAngle.Min"); + this->Vector_CircleRandomAngleMax.Read(exINI, pSection, "Vector.CircleRandomAngle.Max"); + this->Vector_CircleMaxAngle.Read(exINI, pSection, "Vector.CircleMaxAngle"); + this->Vector_CircleMinAngle.Read(exINI, pSection, "Vector.CircleMinAngle"); + this->Vector_TargetOffsetFMin.Read(exINI, pSection, "Vector.TargetOffsetF.Min"); + this->Vector_TargetOffsetFMax.Read(exINI, pSection, "Vector.TargetOffsetF.Max"); + this->Vector_TargetOffsetLMin.Read(exINI, pSection, "Vector.TargetOffsetL.Min"); + this->Vector_TargetOffsetLMax.Read(exINI, pSection, "Vector.TargetOffsetL.Max"); + this->Vector_TargetOffsetHMin.Read(exINI, pSection, "Vector.TargetOffsetH.Min"); + this->Vector_TargetOffsetHMax.Read(exINI, pSection, "Vector.TargetOffsetH.Max"); + this->Vector_ArcRandomHeightMin.Read(exINI, pSection, "Vector.ArcRandomHeight.Min"); + this->Vector_ArcRandomHeightMax.Read(exINI, pSection, "Vector.ArcRandomHeight.Max"); + this->Vector_ArcRandomRotationMin.Read(exINI, pSection, "Vector.ArcRandomRotation.Min"); + this->Vector_ArcRandomRotationMax.Read(exINI, pSection, "Vector.ArcRandomRotation.Max"); + this->Vector_RandomSpeedMin.Read(exINI, pSection, "Vector.RandomSpeed.Min"); + this->Vector_RandomSpeedMax.Read(exINI, pSection, "Vector.RandomSpeed.Max"); + this->Vector_OriginMoveTo.Read(exINI, pSection, "Vector.Origin.MoveTo"); + this->Vector_OriginGrowRate.Read(exINI, pSection, "Vector.Origin.GrowRate"); + this->Vector_OriginTargetFLH.Read(exINI, pSection, "Vector.Origin.TargetFLH"); + this->Vector_OriginInitialSpeed.Read(exINI, pSection, "Vector.Origin.InitialSpeed"); + this->Vector_OriginReachTarget.Read(exINI, pSection, "Vector.Origin.ReachTarget"); + this->Vector_OriginArcHeight.Read(exINI, pSection, "Vector.Origin.ArcHeight"); + this->Vector_OriginCircleRadius.Read(exINI, pSection, "Vector.Origin.CircleRadius"); + this->Vector_OriginCircleSpeed.Read(exINI, pSection, "Vector.Origin.CircleSpeed"); + this->Vector_OriginCircleAnglePerStep.Read(exINI, pSection, "Vector.Origin.CircleAnglePerStep"); + this->Vector_OriginCircleRadiusGrow.Read(exINI, pSection, "Vector.Origin.CircleRadiusGrow"); + this->Vector_OriginCircleMaxRadius.Read(exINI, pSection, "Vector.Origin.CircleMaxRadius"); + this->Vector_OriginCircleMinRadius.Read(exINI, pSection, "Vector.Origin.CircleMinRadius"); + this->Vector_OriginCircleEndOnMaxRadius.Read(exINI, pSection, "Vector.Origin.CircleEndOnMaxRadius"); + this->Vector_OriginCircleEndOnMinRadius.Read(exINI, pSection, "Vector.Origin.CircleEndOnMinRadius"); + this->Vector_OriginNormalVector.Read(exINI, pSection, "Vector.Origin.NormalVector"); + this->Vector_OriginNormalFAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalFAnglePerStep"); + this->Vector_OriginNormalLAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalLAnglePerStep"); + this->Vector_OriginNormalHAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalHAnglePerStep"); + this->Vector_OriginAllowedTilt.Read(exINI, pSection, "Vector.Origin.AllowedTilt"); + this->Vector_OriginCircleOffset.Read(exINI, pSection, "Vector.Origin.CircleOffset"); + this->Vector_OriginOrigin.Read(exINI, pSection, "Vector.Origin.Origin"); + this->Vector_OriginOriginFLH.Read(exINI, pSection, "Vector.Origin.OriginFLH"); + this->Vector_NormalFAngleRMin.Read(exINI, pSection, "Vector.NormalFAngleRanges.Min"); + this->Vector_NormalFAngleRMax.Read(exINI, pSection, "Vector.NormalFAngleRanges.Max"); + this->Vector_NormalFAngleRMin2.Read(exINI, pSection, "Vector.NormalFAngleRanges.Min2"); + this->Vector_NormalFAngleRMax2.Read(exINI, pSection, "Vector.NormalFAngleRanges.Max2"); + this->Vector_NormalLAngleRMin.Read(exINI, pSection, "Vector.NormalLAngleRanges.Min"); + this->Vector_NormalLAngleRMax.Read(exINI, pSection, "Vector.NormalLAngleRanges.Max"); + this->Vector_NormalLAngleRMin2.Read(exINI, pSection, "Vector.NormalLAngleRanges.Min2"); + this->Vector_NormalLAngleRMax2.Read(exINI, pSection, "Vector.NormalLAngleRanges.Max2"); + this->Vector_NormalHAngleRMin.Read(exINI, pSection, "Vector.NormalHAngleRanges.Min"); + this->Vector_NormalHAngleRMax.Read(exINI, pSection, "Vector.NormalHAngleRanges.Max"); + this->Vector_NormalHAngleRMin2.Read(exINI, pSection, "Vector.NormalHAngleRanges.Min2"); + this->Vector_NormalHAngleRMax2.Read(exINI, pSection, "Vector.NormalHAngleRanges.Max2"); + this->Vector_OriginNormalFAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Min"); + this->Vector_OriginNormalFAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Max"); + this->Vector_OriginNormalFAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Min2"); + this->Vector_OriginNormalFAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Max2"); + this->Vector_OriginNormalLAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Min"); + this->Vector_OriginNormalLAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Max"); + this->Vector_OriginNormalLAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Min2"); + this->Vector_OriginNormalLAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Max2"); + this->Vector_OriginNormalHAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Min"); + this->Vector_OriginNormalHAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Max"); + this->Vector_OriginNormalHAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Min2"); + this->Vector_OriginNormalHAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Max2"); + + // Groups + exINI.ParseStringList(this->Groups, pSection, "Groups"); + AddToGroupsMap(); +} + +template +void AttachEffectTypeClass::Serialize(T& Stm) +{ + Stm + .Process(this->Duration) + .Process(this->Duration_ApplyFirepowerMult) + .Process(this->Duration_ApplyArmorMultOnTarget) + .Process(this->Cumulative) + .Process(this->Cumulative_MaxCount) + .Process(this->Powered) + .Process(this->DiscardOn) + .Process(this->DiscardOn_RangeOverride) + .Process(this->DiscardOn_MoveBasedOnDestination) + .Process(this->PenetratesIronCurtain) + .Process(this->PenetratesForceShield) + .Process(this->AffectTypes) + .Process(this->IgnoreTypes) + .Process(this->AffectsTarget) + .Process(this->Animation) + .Process(this->CumulativeAnimations) + .Process(this->CumulativeAnimations_RestartOnChange) + .Process(this->Animation_ResetOnReapply) + .Process(this->Animation_OfflineAction) + .Process(this->Animation_TemporalAction) + .Process(this->Animation_UseInvokerAsOwner) + .Process(this->Animation_HideIfAttachedWith) + .Process(this->ExpireWeapon) + .Process(this->ExpireWeapon_TriggerOn) + .Process(this->ExpireWeapon_CumulativeOnlyOnce) + .Process(this->ExpireWeapon_UseInvokerAsOwner) + .Process(this->Next) + .Process(this->Tint_Color) + .Process(this->Tint_Intensity) + .Process(this->Tint_VisibleToHouses) + .Process(this->FirepowerMultiplier) + .Process(this->ArmorMultiplier) + .Process(this->ArmorMultiplier_AllowWarheads) + .Process(this->ArmorMultiplier_DisallowWarheads) + .Process(this->SpeedMultiplier) + .Process(this->ROFMultiplier) + .Process(this->ROFMultiplier_ApplyOnCurrentTimer) + .Process(this->Cloakable) + .Process(this->ForceDecloak) + .Process(this->WeaponRange_Multiplier) + .Process(this->WeaponRange_ExtraRange) + .Process(this->WeaponRange_AllowWeapons) + .Process(this->WeaponRange_DisallowWeapons) + .Process(this->Crit_Multiplier) + .Process(this->Crit_ExtraChance) + .Process(this->Crit_AllowWarheads) + .Process(this->Crit_DisallowWarheads) + .Process(this->RevengeWeapon) + .Process(this->RevengeWeapon_AffectsHouse) + .Process(this->RevengeWeapon_UseInvokerAsOwner) + .Process(this->ReflectDamage) + .Process(this->ReflectDamage_Warhead) + .Process(this->ReflectDamage_Warhead_Detonate) + .Process(this->ReflectDamage_Multiplier) + .Process(this->ReflectDamage_AffectsHouse) + .Process(this->ReflectDamage_Chance) + .Process(this->ReflectDamage_Override) + .Process(this->ReflectDamage_UseInvokerAsOwner) + .Process(this->DisableWeapons) + .Process(this->Unkillable) + .Process(this->LaserTrail_Type) + .Process(this->Vector_TimeStep) + .Process(this->Vector_DisabledFrames) + .Process(this->Vector_SyncFacing) + .Process(this->Vector_OriginIsOnWorld) + .Process(this->Vector_OriginIsOnBody) + .Process(this->Vector_Origin) + .Process(this->Vector_OriginFLH) + .Process(this->Vector_OriginNoUpdate) + .Process(this->Vector_Force) + .Process(this->Vector_Freeze) + .Process(this->Vector_AllowedTilt) + .Process(this->Vector_NormalVector) + .Process(this->Vector_NormalRandomF) + .Process(this->Vector_NormalRandomL) + .Process(this->Vector_NormalRandomH) + .Process(this->Vector_NormalFAnglePerStep) + .Process(this->Vector_NormalLAnglePerStep) + .Process(this->Vector_NormalHAnglePerStep) + .Process(this->Vector_MoveTo) + .Process(this->Vector_GrowRate) + .Process(this->Vector_AnglePerStep) + .Process(this->Vector_CircleRadius) + .Process(this->Vector_CircleSpeed) + .Process(this->Vector_CircleSpeedAcceleration) + .Process(this->Vector_CircleMaxSpeed) + .Process(this->Vector_CircleMinSpeed) + .Process(this->Vector_CircleAnglePerStep) + .Process(this->Vector_CircleAngleAcceleration) + .Process(this->Vector_CircleOrigin) + .Process(this->Vector_AllowOriginTilt) + .Process(this->Vector_CircleRadiusGrow) + .Process(this->Vector_CircleMaxRadius) + .Process(this->Vector_CircleMinRadius) + .Process(this->Vector_CircleEndOnMaxRadius) + .Process(this->Vector_CircleEndOnMinRadius) + .Process(this->Vector_TargetFLH) + .Process(this->Vector_ReachTarget) + .Process(this->Vector_ReachTargetEarlyEnd) + .Process(this->Vector_ArcHeight) + .Process(this->Vector_ArcPeakPercent) + .Process(this->Vector_ArcPeakRandomPercent) + .Process(this->Vector_ArcRotation) + .Process(this->Vector_InitialSpeed) + .Process(this->Vector_MaxSpeed) + .Process(this->Vector_MinSpeed) + .Process(this->Vector_Acceleration) + .Process(this->Vector_AllowFallingDestroy) + .Process(this->Vector_FallingDestroyHeight) + .Process(this->Vector_AffectTechno) + .Process(this->Vector_AffectBullets) + .Process(this->Vector_CircleRandomRadiusMin) + .Process(this->Vector_CircleRandomRadiusMax) + .Process(this->Vector_CircleRandomAngleMin) + .Process(this->Vector_CircleRandomAngleMax) + .Process(this->Vector_CircleMaxAngle) + .Process(this->Vector_CircleMinAngle) + .Process(this->Vector_TargetOffsetFMin) + .Process(this->Vector_TargetOffsetFMax) + .Process(this->Vector_TargetOffsetLMin) + .Process(this->Vector_TargetOffsetLMax) + .Process(this->Vector_TargetOffsetHMin) + .Process(this->Vector_TargetOffsetHMax) + .Process(this->Vector_ArcRandomHeightMin) + .Process(this->Vector_ArcRandomHeightMax) + .Process(this->Vector_ArcRandomRotationMin) + .Process(this->Vector_ArcRandomRotationMax) + .Process(this->Vector_RandomSpeedMin) + .Process(this->Vector_RandomSpeedMax) + .Process(this->Vector_OriginMoveTo) + .Process(this->Vector_OriginGrowRate) + .Process(this->Vector_OriginTargetFLH) + .Process(this->Vector_OriginInitialSpeed) + .Process(this->Vector_OriginReachTarget) + .Process(this->Vector_OriginArcHeight) + .Process(this->Vector_OriginCircleRadius) + .Process(this->Vector_OriginCircleSpeed) + .Process(this->Vector_OriginCircleAnglePerStep) + .Process(this->Vector_OriginCircleRadiusGrow) + .Process(this->Vector_OriginCircleMaxRadius) + .Process(this->Vector_OriginCircleMinRadius) + .Process(this->Vector_OriginCircleEndOnMaxRadius) + .Process(this->Vector_OriginCircleEndOnMinRadius) + .Process(this->Vector_OriginNormalVector) + .Process(this->Vector_OriginNormalFAnglePerStep) + .Process(this->Vector_OriginNormalLAnglePerStep) + .Process(this->Vector_OriginNormalHAnglePerStep) + .Process(this->Vector_OriginAllowedTilt) + .Process(this->Vector_OriginCircleOffset) + .Process(this->Vector_OriginOrigin) + .Process(this->Vector_OriginOriginFLH) + .Process(this->Vector_NormalFAngleRMin) + .Process(this->Vector_NormalFAngleRMax) + .Process(this->Vector_NormalFAngleRMin2) + .Process(this->Vector_NormalFAngleRMax2) + .Process(this->Vector_NormalLAngleRMin) + .Process(this->Vector_NormalLAngleRMax) + .Process(this->Vector_NormalLAngleRMin2) + .Process(this->Vector_NormalLAngleRMax2) + .Process(this->Vector_NormalHAngleRMin) + .Process(this->Vector_NormalHAngleRMax) + .Process(this->Vector_NormalHAngleRMin2) + .Process(this->Vector_NormalHAngleRMax2) + .Process(this->Vector_OriginNormalFAngleRMin) + .Process(this->Vector_OriginNormalFAngleRMax) + .Process(this->Vector_OriginNormalFAngleRMin2) + .Process(this->Vector_OriginNormalFAngleRMax2) + .Process(this->Vector_OriginNormalLAngleRMin) + .Process(this->Vector_OriginNormalLAngleRMax) + .Process(this->Vector_OriginNormalLAngleRMin2) + .Process(this->Vector_OriginNormalLAngleRMax2) + .Process(this->Vector_OriginNormalHAngleRMin) + .Process(this->Vector_OriginNormalHAngleRMax) + .Process(this->Vector_OriginNormalHAngleRMin2) + .Process(this->Vector_OriginNormalHAngleRMax2) + .Process(this->Groups) + ; +} + +void AttachEffectTypeClass::LoadFromStream(PhobosStreamReader& Stm) +{ + this->Serialize(Stm); + AddToGroupsMap(); +} + +void AttachEffectTypeClass::SaveToStream(PhobosStreamWriter& Stm) +{ + this->Serialize(Stm); +} + +// AE type-related enum etc. parsers +namespace detail +{ + template <> + inline bool read(DiscardCondition& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto parsed = DiscardCondition::None; + + auto str = parser.value(); + char* context = nullptr; + for (auto cur = strtok_s(str, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + if (!_strcmpi(cur, "none")) + { + parsed |= DiscardCondition::None; + } + else if (!_strcmpi(cur, "entry")) + { + parsed |= DiscardCondition::Entry; + } + else if (!_strcmpi(cur, "move")) + { + parsed |= DiscardCondition::Move; + } + else if (!_strcmpi(cur, "stationary")) + { + parsed |= DiscardCondition::Stationary; + } + else if (!_strcmpi(cur, "drain")) + { + parsed |= DiscardCondition::Drain; + } + else if (!_strcmpi(cur, "inrange")) + { + parsed |= DiscardCondition::InRange; + } + else if (!_strcmpi(cur, "outofrange")) + { + parsed |= DiscardCondition::OutOfRange; + } + else if (!_strcmpi(cur, "firing")) + { + parsed |= DiscardCondition::Firing; + } + else + { + Debug::INIParseFailed(pSection, pKey, cur, "Expected a discard condition type"); + return false; + } + } + + value = parsed; + return true; + } + + return false; + } + + template <> + inline bool read(ExpireWeaponCondition& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto parsed = ExpireWeaponCondition::None; + + auto str = parser.value(); + char* context = nullptr; + for (auto cur = strtok_s(str, Phobos::readDelims, &context); cur; cur = strtok_s(nullptr, Phobos::readDelims, &context)) + { + if (!_strcmpi(cur, "none")) + { + parsed |= ExpireWeaponCondition::None; + } + else if (!_strcmpi(cur, "expire")) + { + parsed |= ExpireWeaponCondition::Expire; + } + else if (!_strcmpi(cur, "remove")) + { + parsed |= ExpireWeaponCondition::Remove; + } + else if (!_strcmpi(cur, "death")) + { + parsed |= ExpireWeaponCondition::Death; + } + else if (!_strcmpi(cur, "discard")) + { + parsed |= ExpireWeaponCondition::Discard; + } + else if (!_strcmpi(cur, "all")) + { + parsed |= ExpireWeaponCondition::All; + } + else + { + Debug::INIParseFailed(pSection, pKey, cur, "Expected a expire weapon trigger condition type"); + return false; + } + } + + value = parsed; + return true; + } + + return false; + } + + template <> + inline bool read(VectorOrigin& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto str = parser.value(); + + if (!_strcmpi(str, "Self")) + value = VectorOrigin::Self; + else if (!_strcmpi(str, "Launcher")) + value = VectorOrigin::Launcher; + else if (!_strcmpi(str, "Target")) + value = VectorOrigin::Target; + else if (!_strcmpi(str, "Source")) + value = VectorOrigin::Source; + else + value = VectorOrigin::Self; + + return true; + } + + return false; + } +} + +// AEAttachInfoTypeClass + +void AEAttachInfoTypeClass::LoadFromINI(CCINIClass* pINI, const char* pSection) +{ + INI_EX exINI(pINI); + + this->AttachTypes.Read(exINI, pSection, "AttachEffect.AttachTypes"); + this->CumulativeSourceMaxCount.Read(exINI, pSection, "AttachEffect.CumulativeSourceMaxCount"); + this->CumulativeRefreshAll.Read(exINI, pSection, "AttachEffect.CumulativeRefreshAll"); + this->CumulativeRefreshAll_OnAttach.Read(exINI, pSection, "AttachEffect.CumulativeRefreshAll.OnAttach"); + this->CumulativeRefreshSameSourceOnly.Read(exINI, pSection, "AttachEffect.CumulativeRefreshSameSourceOnly"); + this->RemoveTypes.Read(exINI, pSection, "AttachEffect.RemoveTypes"); + exINI.ParseStringList(this->RemoveGroups, pSection, "AttachEffect.RemoveGroups"); + this->CumulativeRemoveMinCounts.Read(exINI, pSection, "AttachEffect.CumulativeRemoveMinCounts"); + this->CumulativeRemoveMaxCounts.Read(exINI, pSection, "AttachEffect.CumulativeRemoveMaxCounts"); + this->DurationOverrides.Read(exINI, pSection, "AttachEffect.DurationOverrides"); + this->Delays.Read(exINI, pSection, "AttachEffect.Delays"); + this->InitialDelays.Read(exINI, pSection, "AttachEffect.InitialDelays"); + this->RecreationDelays.Read(exINI, pSection, "AttachEffect.RecreationDelays"); +} + +AEAttachParams AEAttachInfoTypeClass::GetAttachParams(unsigned int index, bool selfOwned) const +{ + AEAttachParams info { }; + + if (this->DurationOverrides.size() > 0) + info.DurationOverride = this->DurationOverrides[this->DurationOverrides.size() > index ? index : this->DurationOverrides.size() - 1]; + + if (selfOwned) + { + if (this->Delays.size() > 0) + info.Delay = this->Delays[this->Delays.size() > index ? index : this->Delays.size() - 1]; + + if (this->InitialDelays.size() > 0) + info.InitialDelay = this->InitialDelays[this->InitialDelays.size() > index ? index : this->InitialDelays.size() - 1]; + + if (this->RecreationDelays.size() > 0) + info.RecreationDelay = this->RecreationDelays[this->RecreationDelays.size() > index ? index : this->RecreationDelays.size() - 1]; + } + else + { + info.CumulativeSourceMaxCount = this->CumulativeSourceMaxCount; + info.CumulativeRefreshAll = this->CumulativeRefreshAll; + info.CumulativeRefreshAll_OnAttach = this->CumulativeRefreshAll_OnAttach; + info.CumulativeRefreshSameSourceOnly = this->CumulativeRefreshSameSourceOnly; + } + + return info; +} + +#pragma region(save/load) + +template +bool AEAttachInfoTypeClass::Serialize(T& stm) +{ + return stm + .Process(this->AttachTypes) + .Process(this->CumulativeSourceMaxCount) + .Process(this->CumulativeRefreshAll) + .Process(this->CumulativeRefreshAll_OnAttach) + .Process(this->CumulativeRefreshSameSourceOnly) + .Process(this->RemoveTypes) + .Process(this->RemoveGroups) + .Process(this->CumulativeRemoveMinCounts) + .Process(this->CumulativeRemoveMaxCounts) + .Process(this->DurationOverrides) + .Process(this->Delays) + .Process(this->InitialDelays) + .Process(this->RecreationDelays) + .Success(); +} + +bool AEAttachInfoTypeClass::Load(PhobosStreamReader& stm, bool registerForChange) +{ + return this->Serialize(stm); +} + +bool AEAttachInfoTypeClass::Save(PhobosStreamWriter& stm) const +{ + return const_cast(this)->Serialize(stm); +} + +#pragma endregion(save/load) diff --git a/VectorPort/src/New/Type/AttachEffectTypeClass.h b/VectorPort/src/New/Type/AttachEffectTypeClass.h new file mode 100644 index 0000000000..5f4c624bfd --- /dev/null +++ b/VectorPort/src/New/Type/AttachEffectTypeClass.h @@ -0,0 +1,529 @@ +#pragma once + +#include +#include + +#include +#include +#include "LaserTrailTypeClass.h" + +// AE discard condition +enum class DiscardCondition : unsigned char +{ + None = 0x0, + Entry = 0x1, + Move = 0x2, + Stationary = 0x4, + Drain = 0x8, + InRange = 0x10, + OutOfRange = 0x20, + Firing = 0x40 +}; + +MAKE_ENUM_FLAGS(DiscardCondition); + +// AE expire weapon condition +enum class ExpireWeaponCondition : unsigned char +{ + None = 0x0, + Expire = 0x1, + Remove = 0x2, + Death = 0x4, + Discard = 0x8, + + All = 0xFF, +}; + +MAKE_ENUM_FLAGS(ExpireWeaponCondition); + +enum class VectorOrigin : int +{ + Self = 0, + Launcher = 1, + Target = 2, + Source = 3 +}; + +class AttachEffectTypeClass final : public Enumerable +{ + static std::unordered_map> GroupsMap; + +public: + Valueable Duration; + Valueable Duration_ApplyFirepowerMult; + Valueable Duration_ApplyArmorMultOnTarget; + Valueable Cumulative; + Valueable Cumulative_MaxCount; + Valueable Powered; + Valueable DiscardOn; + Nullable DiscardOn_RangeOverride; + Nullable DiscardOn_MoveBasedOnDestination; + Valueable PenetratesIronCurtain; + Nullable PenetratesForceShield; + ValueableVector AffectTypes; + ValueableVector IgnoreTypes; + Valueable AffectsTarget; + Valueable Animation; + ValueableVector CumulativeAnimations; + Valueable CumulativeAnimations_RestartOnChange; + Valueable Animation_ResetOnReapply; + Valueable Animation_OfflineAction; + Valueable Animation_TemporalAction; + Valueable Animation_UseInvokerAsOwner; + ValueableVector Animation_HideIfAttachedWith; + Valueable ExpireWeapon; + Valueable ExpireWeapon_TriggerOn; + Valueable ExpireWeapon_CumulativeOnlyOnce; + Valueable ExpireWeapon_UseInvokerAsOwner; + ValueableVector Next; + Nullable Tint_Color; + Valueable Tint_Intensity; + Valueable Tint_VisibleToHouses; + Valueable FirepowerMultiplier; + Valueable ArmorMultiplier; + ValueableVector ArmorMultiplier_AllowWarheads; + ValueableVector ArmorMultiplier_DisallowWarheads; + Valueable SpeedMultiplier; + Valueable ROFMultiplier; + Valueable ROFMultiplier_ApplyOnCurrentTimer; + Valueable Cloakable; + Valueable ForceDecloak; + Valueable WeaponRange_Multiplier; + Valueable WeaponRange_ExtraRange; + ValueableVector WeaponRange_AllowWeapons; + ValueableVector WeaponRange_DisallowWeapons; + Valueable Crit_Multiplier; + Valueable Crit_ExtraChance; + ValueableVector Crit_AllowWarheads; + ValueableVector Crit_DisallowWarheads; + Valueable RevengeWeapon; + Valueable RevengeWeapon_AffectsHouse; + Valueable RevengeWeapon_UseInvokerAsOwner; + Valueable ReflectDamage; + Nullable ReflectDamage_Warhead; + Valueable ReflectDamage_Warhead_Detonate; + Valueable ReflectDamage_Multiplier; + Valueable ReflectDamage_AffectsHouse; + Valueable ReflectDamage_Chance; + Nullable ReflectDamage_Override; + Valueable ReflectDamage_UseInvokerAsOwner; + Valueable DisableWeapons; + Valueable Unkillable; + ValueableIdx LaserTrail_Type; + + Valueable Vector_TimeStep; + Valueable Vector_DisabledFrames; + Valueable Vector_SyncFacing; + Valueable Vector_OriginIsOnWorld; + Valueable Vector_OriginIsOnBody; + Valueable Vector_Origin; + Nullable Vector_OriginFLH; + Valueable Vector_OriginNoUpdate; + Valueable Vector_Force; + Valueable Vector_Freeze; + Valueable Vector_AllowedTilt; + Nullable Vector_NormalVector; + Valueable Vector_NormalRandomF; + Valueable Vector_NormalRandomL; + Valueable Vector_NormalRandomH; + Valueable Vector_NormalFAnglePerStep; + Valueable Vector_NormalLAnglePerStep; + Valueable Vector_NormalHAnglePerStep; + Valueable Vector_MoveTo; + Nullable Vector_GrowRate; + Valueable Vector_AnglePerStep; + Valueable Vector_CircleRadius; + Valueable Vector_CircleSpeed; + Valueable Vector_CircleSpeedAcceleration; + Valueable Vector_CircleMaxSpeed; + Valueable Vector_CircleMinSpeed; + Valueable Vector_CircleAnglePerStep; + Valueable Vector_CircleAngleAcceleration; + Nullable Vector_CircleOrigin; + Valueable Vector_AllowOriginTilt; + Valueable Vector_CircleRadiusGrow; + Valueable Vector_CircleMaxRadius; + Valueable Vector_CircleMinRadius; + Valueable Vector_CircleEndOnMaxRadius; + Valueable Vector_CircleEndOnMinRadius; + Nullable Vector_TargetFLH; + Valueable Vector_ReachTarget; + Valueable Vector_ReachTargetEarlyEnd; + Valueable Vector_ArcHeight; + Valueable Vector_ArcPeakPercent; + Valueable Vector_ArcPeakRandomPercent; + Valueable Vector_ArcRotation; + Valueable Vector_InitialSpeed; + Valueable Vector_MaxSpeed; + Valueable Vector_MinSpeed; + Valueable Vector_Acceleration; + Valueable Vector_AllowFallingDestroy; + Valueable Vector_FallingDestroyHeight; + Valueable Vector_AffectTechno; + Valueable Vector_AffectBullets; + Valueable Vector_CircleRandomRadiusMin; + Valueable Vector_CircleRandomRadiusMax; + Valueable Vector_CircleRandomAngleMin; + Valueable Vector_CircleRandomAngleMax; + Valueable Vector_CircleMaxAngle; + Valueable Vector_CircleMinAngle; + Valueable Vector_TargetOffsetFMin; + Valueable Vector_TargetOffsetFMax; + Valueable Vector_TargetOffsetLMin; + Valueable Vector_TargetOffsetLMax; + Valueable Vector_TargetOffsetHMin; + Valueable Vector_TargetOffsetHMax; + Valueable Vector_ArcRandomHeightMin; + Valueable Vector_ArcRandomHeightMax; + Valueable Vector_ArcRandomRotationMin; + Valueable Vector_ArcRandomRotationMax; + Valueable Vector_RandomSpeedMin; + Valueable Vector_RandomSpeedMax; + Nullable Vector_OriginMoveTo; + Nullable Vector_OriginGrowRate; + Nullable Vector_OriginTargetFLH; + Valueable Vector_OriginInitialSpeed; + Valueable Vector_OriginReachTarget; + Valueable Vector_OriginArcHeight; + Valueable Vector_OriginCircleRadius; + Valueable Vector_OriginCircleSpeed; + Valueable Vector_OriginCircleAnglePerStep; + Valueable Vector_OriginCircleRadiusGrow; + Valueable Vector_OriginCircleMaxRadius; + Valueable Vector_OriginCircleMinRadius; + Valueable Vector_OriginCircleEndOnMaxRadius; + Valueable Vector_OriginCircleEndOnMinRadius; + Nullable Vector_OriginNormalVector; + Valueable Vector_OriginNormalFAnglePerStep; + Valueable Vector_OriginNormalLAnglePerStep; + Valueable Vector_OriginNormalHAnglePerStep; + Valueable Vector_OriginAllowedTilt; + Nullable Vector_OriginCircleOffset; + Valueable Vector_OriginOrigin; + Nullable Vector_OriginOriginFLH; + Valueable Vector_NormalFAngleRMin; + Valueable Vector_NormalFAngleRMax; + Valueable Vector_NormalFAngleRMin2; + Valueable Vector_NormalFAngleRMax2; + Valueable Vector_NormalLAngleRMin; + Valueable Vector_NormalLAngleRMax; + Valueable Vector_NormalLAngleRMin2; + Valueable Vector_NormalLAngleRMax2; + Valueable Vector_NormalHAngleRMin; + Valueable Vector_NormalHAngleRMax; + Valueable Vector_NormalHAngleRMin2; + Valueable Vector_NormalHAngleRMax2; + Valueable Vector_OriginNormalFAngleRMin; + Valueable Vector_OriginNormalFAngleRMax; + Valueable Vector_OriginNormalFAngleRMin2; + Valueable Vector_OriginNormalFAngleRMax2; + Valueable Vector_OriginNormalLAngleRMin; + Valueable Vector_OriginNormalLAngleRMax; + Valueable Vector_OriginNormalLAngleRMin2; + Valueable Vector_OriginNormalLAngleRMax2; + Valueable Vector_OriginNormalHAngleRMin; + Valueable Vector_OriginNormalHAngleRMax; + Valueable Vector_OriginNormalHAngleRMin2; + Valueable Vector_OriginNormalHAngleRMax2; + + std::vector Groups; + + AttachEffectTypeClass(const char* const pTitle) : Enumerable(pTitle) + , Duration { 0 } + , Duration_ApplyFirepowerMult { false } + , Duration_ApplyArmorMultOnTarget { false } + , Cumulative { false } + , Cumulative_MaxCount { -1 } + , Powered { false } + , DiscardOn { DiscardCondition::None } + , DiscardOn_RangeOverride {} + , DiscardOn_MoveBasedOnDestination {} + , PenetratesIronCurtain { false } + , PenetratesForceShield {} + , AffectTypes {} + , IgnoreTypes {} + , AffectsTarget { AffectedTarget::All } + , Animation {} + , CumulativeAnimations {} + , CumulativeAnimations_RestartOnChange { true } + , Animation_ResetOnReapply { false } + , Animation_OfflineAction { AttachedAnimFlag::Hides } + , Animation_TemporalAction { AttachedAnimFlag::None } + , Animation_UseInvokerAsOwner { false } + , Animation_HideIfAttachedWith {} + , ExpireWeapon {} + , ExpireWeapon_TriggerOn { ExpireWeaponCondition::Expire } + , ExpireWeapon_CumulativeOnlyOnce { false } + , ExpireWeapon_UseInvokerAsOwner { false } + , Next {} + , Tint_Color {} + , Tint_Intensity { 0.0 } + , Tint_VisibleToHouses { AffectedHouse::All } + , FirepowerMultiplier { 1.0 } + , ArmorMultiplier { 1.0 } + , ArmorMultiplier_AllowWarheads {} + , ArmorMultiplier_DisallowWarheads {} + , SpeedMultiplier { 1.0 } + , ROFMultiplier { 1.0 } + , ROFMultiplier_ApplyOnCurrentTimer { true } + , Cloakable { false } + , ForceDecloak { false } + , WeaponRange_Multiplier { 1.0 } + , WeaponRange_ExtraRange { 0.0 } + , WeaponRange_AllowWeapons {} + , WeaponRange_DisallowWeapons {} + , Crit_Multiplier { 1.0 } + , Crit_ExtraChance { 0.0 } + , Crit_AllowWarheads {} + , Crit_DisallowWarheads {} + , RevengeWeapon {} + , RevengeWeapon_AffectsHouse { AffectedHouse::All } + , ReflectDamage { false } + , RevengeWeapon_UseInvokerAsOwner { false } + , ReflectDamage_Warhead {} + , ReflectDamage_Warhead_Detonate { false } + , ReflectDamage_Multiplier { 1.0 } + , ReflectDamage_AffectsHouse { AffectedHouse::All } + , ReflectDamage_Chance { 1.0 } + , ReflectDamage_Override {} + , ReflectDamage_UseInvokerAsOwner { false } + , DisableWeapons { false } + , Unkillable { false } + , LaserTrail_Type { -1 } + , Vector_TimeStep { 1 } + , Vector_DisabledFrames { 0 } + , Vector_SyncFacing { true } + , Vector_OriginIsOnWorld { false } + , Vector_OriginIsOnBody { false } + , Vector_Origin { VectorOrigin::Self } + , Vector_OriginFLH {} + , Vector_OriginNoUpdate { false } + , Vector_Force { true } + , Vector_Freeze { false } + , Vector_AllowedTilt { false } + , Vector_NormalVector {} + , Vector_NormalRandomF { 0.0 } + , Vector_NormalRandomL { 0.0 } + , Vector_NormalRandomH { 0.0 } + , Vector_NormalFAnglePerStep { 0.0 } + , Vector_NormalLAnglePerStep { 0.0 } + , Vector_NormalHAnglePerStep { 0.0 } + , Vector_MoveTo { CoordStruct::Empty } + , Vector_GrowRate {} + , Vector_AnglePerStep { 0.0 } + , Vector_CircleRadius { -1 } + , Vector_CircleSpeed { 0 } + , Vector_CircleSpeedAcceleration { 0 } + , Vector_CircleMaxSpeed { 0 } + , Vector_CircleMinSpeed { 0 } + , Vector_CircleAnglePerStep { 0.0 } + , Vector_CircleAngleAcceleration { 0.0 } + , Vector_CircleOrigin {} + , Vector_AllowOriginTilt { false } + , Vector_CircleRadiusGrow { 0 } + , Vector_CircleMaxRadius { 0 } + , Vector_CircleMinRadius { 0 } + , Vector_CircleEndOnMaxRadius { false } + , Vector_CircleEndOnMinRadius { false } + , Vector_TargetFLH {} + , Vector_ReachTarget { false } + , Vector_ReachTargetEarlyEnd { 0 } + , Vector_ArcHeight { 0 } + , Vector_ArcPeakPercent { 0.0 } + , Vector_ArcPeakRandomPercent { 0.0 } + , Vector_ArcRotation { 0.0 } + , Vector_InitialSpeed { -1 } + , Vector_MaxSpeed { -1 } + , Vector_MinSpeed { -1 } + , Vector_Acceleration { 0 } + , Vector_AllowFallingDestroy { false } + , Vector_FallingDestroyHeight { 0 } + , Vector_AffectTechno { true } + , Vector_AffectBullets { false } + , Vector_CircleRandomRadiusMin { -1 } + , Vector_CircleRandomRadiusMax { -1 } + , Vector_CircleRandomAngleMin { -1 } + , Vector_CircleRandomAngleMax { -1 } + , Vector_CircleMaxAngle { 0.0 } + , Vector_CircleMinAngle { 0.0 } + , Vector_TargetOffsetFMin { 0 } + , Vector_TargetOffsetFMax { 0 } + , Vector_TargetOffsetLMin { 0 } + , Vector_TargetOffsetLMax { 0 } + , Vector_TargetOffsetHMin { 0 } + , Vector_TargetOffsetHMax { 0 } + , Vector_ArcRandomHeightMin { 0 } + , Vector_ArcRandomHeightMax { 0 } + , Vector_ArcRandomRotationMin { 0.0 } + , Vector_ArcRandomRotationMax { 0.0 } + , Vector_RandomSpeedMin { -1 } + , Vector_RandomSpeedMax { -1 } + , Vector_OriginMoveTo {} + , Vector_OriginGrowRate {} + , Vector_OriginTargetFLH {} + , Vector_OriginInitialSpeed { -1 } + , Vector_OriginReachTarget { false } + , Vector_OriginArcHeight { 0 } + , Vector_OriginCircleRadius { -1 } + , Vector_OriginCircleSpeed { 0 } + , Vector_OriginCircleAnglePerStep { 0.0 } + , Vector_OriginCircleRadiusGrow { 0 } + , Vector_OriginCircleMaxRadius { 0 } + , Vector_OriginCircleMinRadius { 0 } + , Vector_OriginCircleEndOnMaxRadius { false } + , Vector_OriginCircleEndOnMinRadius { false } + , Vector_OriginNormalVector {} + , Vector_OriginNormalFAnglePerStep { 0.0 } + , Vector_OriginNormalLAnglePerStep { 0.0 } + , Vector_OriginNormalHAnglePerStep { 0.0 } + , Vector_OriginAllowedTilt { false } + , Vector_OriginCircleOffset {} + , Vector_OriginOrigin { VectorOrigin::Self } + , Vector_OriginOriginFLH {} + , Vector_NormalFAngleRMin { 0.0 } + , Vector_NormalFAngleRMax { 0.0 } + , Vector_NormalFAngleRMin2 { 0.0 } + , Vector_NormalFAngleRMax2 { 0.0 } + , Vector_NormalLAngleRMin { 0.0 } + , Vector_NormalLAngleRMax { 0.0 } + , Vector_NormalLAngleRMin2 { 0.0 } + , Vector_NormalLAngleRMax2 { 0.0 } + , Vector_NormalHAngleRMin { 0.0 } + , Vector_NormalHAngleRMax { 0.0 } + , Vector_NormalHAngleRMin2 { 0.0 } + , Vector_NormalHAngleRMax2 { 0.0 } + , Vector_OriginNormalFAngleRMin { 0.0 } + , Vector_OriginNormalFAngleRMax { 0.0 } + , Vector_OriginNormalFAngleRMin2 { 0.0 } + , Vector_OriginNormalFAngleRMax2 { 0.0 } + , Vector_OriginNormalLAngleRMin { 0.0 } + , Vector_OriginNormalLAngleRMax { 0.0 } + , Vector_OriginNormalLAngleRMin2 { 0.0 } + , Vector_OriginNormalLAngleRMax2 { 0.0 } + , Vector_OriginNormalHAngleRMin { 0.0 } + , Vector_OriginNormalHAngleRMax { 0.0 } + , Vector_OriginNormalHAngleRMin2 { 0.0 } + , Vector_OriginNormalHAngleRMax2 { 0.0 } + , Groups {} + {}; + + bool HasTint() const + { + return this->Tint_Color.isset() || this->Tint_Intensity != 0.0; + } + + bool HasVector() const + { + return static_cast(this->Vector_MoveTo) != CoordStruct::Empty + || this->Vector_TargetFLH.isset() + || this->Vector_Freeze + || this->Vector_ReachTarget + || this->Vector_CircleRadius > 0 + || this->Vector_CircleSpeed != 0 + || this->Vector_CircleAnglePerStep > 0.0 + || (this->Vector_CircleRandomRadiusMax > this->Vector_CircleRandomRadiusMin) + || (this->Vector_CircleRandomAngleMax > this->Vector_CircleRandomAngleMin) + || this->Vector_OriginMoveTo.isset(); + } + + bool HasGroup(const std::string& groupID) const; + bool HasGroups(const std::vector& groupIDs, bool requireAll) const; + + AnimTypeClass* GetCumulativeAnimation(int cumulativeCount) const + { + if (cumulativeCount < 0 || this->CumulativeAnimations.size() < 1) + return nullptr; + + const int index = static_cast(cumulativeCount) >= this->CumulativeAnimations.size() ? this->CumulativeAnimations.size() - 1 : cumulativeCount - 1; + + return this->CumulativeAnimations.at(index); + } + + void LoadFromINI(CCINIClass* pINI); + void LoadFromStream(PhobosStreamReader& Stm); + void SaveToStream(PhobosStreamWriter& Stm); + + static void Clear() + { + AttachEffectTypeClass::GroupsMap.clear(); + } + + static std::vector GetTypesFromGroups(const std::vector& groupIDs); + static void HandleEvent(TechnoClass* pTarget); + +private: + template + void Serialize(T& Stm); + void AddToGroupsMap(); +}; + +// Container for AttachEffect attachment for an individual effect passed to AE attach function. +struct AEAttachParams +{ + int DurationOverride; + int Delay; + int InitialDelay; + int RecreationDelay; + int CumulativeSourceMaxCount; + bool CumulativeRefreshAll; + bool CumulativeRefreshAll_OnAttach; + bool CumulativeRefreshSameSourceOnly; + + AEAttachParams() : + DurationOverride { 0 } + , Delay { 0 } + , InitialDelay { 0 } + , RecreationDelay { -1 } + , CumulativeSourceMaxCount { -1 } + , CumulativeRefreshAll { false } + , CumulativeRefreshAll_OnAttach { false } + , CumulativeRefreshSameSourceOnly { true } + { + } +}; + +// Container for AttachEffect attachment info parsed from INI. +class AEAttachInfoTypeClass +{ +public: + ValueableVector AttachTypes; + Valueable CumulativeSourceMaxCount; + Valueable CumulativeRefreshAll; + Valueable CumulativeRefreshAll_OnAttach; + Valueable CumulativeRefreshSameSourceOnly; + ValueableVector RemoveTypes; + std::vector RemoveGroups; + ValueableVector CumulativeRemoveMinCounts; + ValueableVector CumulativeRemoveMaxCounts; + ValueableVector DurationOverrides; + ValueableVector Delays; + ValueableVector InitialDelays; + ValueableVector RecreationDelays; + + void LoadFromINI(CCINIClass* pINI, const char* pSection); + bool Load(PhobosStreamReader& stm, bool registerForChange); + bool Save(PhobosStreamWriter& stm) const; + + AEAttachParams GetAttachParams(unsigned int index, bool selfOwned) const; + + AEAttachInfoTypeClass() : + AttachTypes {} + , CumulativeSourceMaxCount { -1 } + , CumulativeRefreshAll { false } + , CumulativeRefreshAll_OnAttach { false } + , CumulativeRefreshSameSourceOnly { true } + , RemoveTypes {} + , RemoveGroups {} + , CumulativeRemoveMinCounts {} + , CumulativeRemoveMaxCounts {} + , DurationOverrides {} + , Delays {} + , InitialDelays {} + , RecreationDelays {} + { + } + +private: + template + bool Serialize(T& stm); +}; diff --git a/Vector_FixList.md b/Vector_FixList.md new file mode 100644 index 0000000000..d06d9978f1 --- /dev/null +++ b/Vector_FixList.md @@ -0,0 +1,153 @@ +# Phobos Vector 待修问题清单 +> 基于 KratosAI `src/Ext/EffectType/Effect/VectorEffect.cpp` 与 PhobosAI `src/New/Entity/VectorState.cpp` 逐行对比。 +> 所有行号以当前 feature/vector-port 分支为准。 + +--- + +## 🔴 B1 — OriginNormalVector atan2 参数反了 +**文件**: `src/New/Entity/VectorState.cpp:419` +**问题**: `std::atan2(nv.Y, nv.X)` — 参数顺序是 `atan2(L, F)`,应为 `atan2(F, L)`。 +**参考 Kratos**: `VectorEffect.cpp:587` — `std::atan2(fy, fx)` where `fy = OriginNormalVector.X (F), fx = .Y (L)` +**修复**: 改为 `std::atan2(nv.X, nv.Y)` +**影响**: 原点法向量旋转 90°,圆心方向错误。 + +--- + +## 🔴 B2 — pInvoker 存活检查缺失 +**文件**: `src/New/Entity/VectorState.cpp:256` +**问题**: `auto const pLT = static_cast(pInvoker)` 无 null/存活检查,pInvoker 死亡时直接崩溃。 +**参考 Kratos**: `VectorEffect.cpp:400-402` — `if (_pLauncher && !IsDeadOrInvisible(_pLauncher)) { TechnoClass* pLauncherTechno = abstract_cast(_pLauncher); }` +**修复**: 在 `static_cast` 前加 `if (pInvoker && !pInvoker->IsAlive)` 守卫,或换成 `abstract_cast`。 + +--- + +## 🔴 B3 — 动态 facing 地面子弹无 TargetCoords 回退 +**文件**: `src/New/Entity/VectorState.cpp:211` +**问题**: `if (pB->Target)` 只检查 Techno* 目标,地面对 TargetCoords 无回退。 +**参考 Kratos**: `VectorEffect.cpp:364-371` — 区分 ground 和 Techno,ground 用 `TargetCoords`。 +**修复**: 改为 `pB->Target ? pB->Target->GetCoords() : pB->TargetCoords` + +--- + +## 🟡 B4 — Origin=Launcher/Target/Source 初始 facing 缺失 +**文件**: `src/New/Entity/VectorState.cpp:87-104` +**问题**: 初始化时统一用 bullet velocity 或 PrimaryFacing,未按 Origin 类型分别设置 facing。 +**参考 Kratos**: `VectorEffect.cpp:180-259` — 完整的 switch(Origin) 分支,Launcher 用发射者朝向,Target 用自→目标方向,Source 用源→自方向,均含 AllowedTilt tilt 计算。 +**修复**: 在 `s.Initialized` 块中,模仿 Kratos 的 switch 分支设置 `s.FacingRad` 和 `s.TiltRad`(用于 OriginNoUpdate 或无 NormalVector 的初始 facing)。 + +--- + +## 🟡 B5 — 初始速度无 techno/bullet 回退 +**文件**: `src/New/Entity/VectorState.cpp:106` +**问题**: `InitialSpeed >= 0 ? InitialSpeed : 0` — 默认 0,不读单位/弹体原生速度。 +**参考 Kratos**: `VectorEffect.cpp:60-71` — 未设 InitialSpeed 时回退到 `pType->JumpjetSpeed / Speed` 或 `pBullet->Speed`。 +**修复**: InitialSpeed < 0 时从 pObject 读原生速度。 + +--- + +## 🟡 B6 — Self 模式 InitialFacing 用 Primary 而非 Turret +**文件**: `src/New/Entity/VectorState.cpp:100` +**问题**: `pT->PrimaryFacing.Current().GetRadian<32>()` 应改为 `TurretFacing()`. +**参考 Kratos**: `VectorEffect.cpp:389` — 有 `OriginIsOnBody` 控制,默认 TurretFacing。 +**修复**: 如 OriginIsOnBody=false,用 `pT->TurretFacing().Current()`。 + +--- + +## 🟡 B7 — remainingFrames 差 1 帧 +**文件**: `src/New/Entity/VectorState.cpp:687` +**问题**: `effectiveDuration - s.MovementFrames` 应改为 `effectiveDuration - s.MovementFrames + 1`。 +**参考 Kratos**: `VectorEffect.cpp:930` — `effectiveDuration - _movementFrames + 1`。 +**修复**: 加 `+1`。 + +--- + +## 🟡 B8 — Source+OriginNoUpdate 初始位置未初始化 +**文件**: `src/New/Entity/VectorState.cpp:58-70` +**问题**: `s.InitialOriginPos` 只处理了 Launcher 和 Target 分支,无 Source 分支。 +**参考 Kratos**: `VectorEffect.cpp:116-124` — Source 时读 `AE->pSource->GetCoords()`。 +**修复**: 添加 `case VectorOrigin::Source:` 分支,从 pInvoker (对子弹即为 AE source) 取坐标。 + +--- + +## 🟠 M1 — 圆随机半径用 int trunc +**文件**: `src/New/Entity/VectorState.cpp:325` +**问题**: `rand() % (Max - Min + 1)` 整数截断。 +**参考 Kratos**: `VectorEffect.cpp:780` — `Random::RandomRanged` (double)。 +**修复**: 改用 `V_Random(Min, Max)`。 + +--- + +## 🟠 M2 — OriginLissajous 缺失 +**文件**: `src/New/Entity/VectorState.cpp:428-442` (圆形移动段) +**问题**: Phobos 始终用 smooth 增量旋转,无 Lissajous 模式。 +**参考 Kratos**: `VectorEffect.cpp:703-704` — `Data->OriginLissajous ? cumulativeAngle : incrementOnly`。 +**修复**: 添加 `Vector_OriginLissajous` 标签,按 Kratos 逻辑分支处理。 + +--- + +## 🟠 M3 — Origin ReachTarget + ArcHeight 缺失 +**文件**: `src/New/Entity/VectorState.cpp:407-455` +**问题**: 圆心只支持 Speed 模式(恒速追踪),无 ReachTarget 模式及弧高。 +**参考 Kratos**: `VectorEffect.cpp:654-679` — OriginReachTarget 分支含 remainingFrames 分批到达及 `OriginArcHeight` 弧高计算。 +**修复**: 添加完整的 OriginReachTarget 分支代码。 + +--- + +## 🟠 M4 — OriginCircleOffset 缺失 +**问题**: Phobos 无 `Vector_OriginCircleOffset` 标签。 +**参考 Kratos**: `VectorEffect.cpp:559-560` — `baseCenter += Data->OriginCircleOffset` (世界偏移)。 +**修复**: 新增标签及读取,在 baseCenter 计算后叠加。 + +--- + +## 🟠 M5 — OriginTargetOffset 随机化缺失 +**文件**: `src/New/Entity/VectorState.cpp:413` +**问题**: `s.OriginTargetOffset = CoordStruct::Empty` 未赋值。 +**参考 Kratos**: `VectorEffect.cpp:576-578` — 每轴独立随机范围。 +**修复**: 初始化时按 `OriginTargetOffsetF/L/H Min/Max` 随机赋值。 + +--- + +## 🟠 M6 — Origin 加速度/钳位缺失 +**问题**: Phobos 圆心速度恒定为 InitialSpeed,无加速度和上下限。 +**参考 Kratos**: `VectorEffect.cpp:683-685` — `OriginAcceleration`、`OriginMaxSpeed`、`OriginMinSpeed`。 +**修复**: 新增标签,Speed 模式每帧叠加加速度并钳位。 + +--- + +## 🟠 M7 — NormalCircleRadius 哨兵缺失 +**问题**: 有 NormalVector 但无明确 CircleRadius 时不会自动推断。 +**参考 Kratos**: `VectorEffect.cpp:595-596` — 设 `_originCircleRadius = effectiveFacing > -1 ? 0 : -1` 作为哨兵。 +**修复**: 在初始化时加类似哨兵逻辑。 + +--- + +## 🟠 M8 — ReachTargetEarlyEnd 死循环 +**文件**: `src/New/Entity/VectorState.cpp:689-693` +**问题**: DisabledTimer 归零后 remainingFrames 不变,反复触发 EarlyEnd。 +**参考 Kratos**: `VectorEffect.cpp:934` — `Deactivate()` 直接终止,不设 timer。 +**修复**: 改为一次性 `s.DisabledTimer = EarlyEnd` 后立即递增 `s.MovementFrames` 以退出循环区间,或直接 `Deactivate()`。 + +--- + +## 🟠 M9 — 建筑对象无排除 +**文件**: `src/New/Entity/VectorState.cpp:33` (VectorAI_Run 入口) +**问题**: 无 building 类型检查。 +**参考 Kratos**: `VectorEffect.cpp:19-23` — Building 对象直接 `Deactivate()`。 +**修复**: 入口加 `if (pObject->WhatAmI() == AbstractType::Building) return;` + +--- + +## 🟠 M10 — 圆模式双区间随机角速度缺失 +**问题**: Phobos 只有单区间 `CircleRandomAngleMin/Max`。 +**参考 Kratos**: `VectorEffect.cpp:485-489` — 4 值区间 `CircleRandomAngleMin/Max` + `Min2/Max2`。 +**修复**: 新增标签及初始化时的 4 值随机逻辑。 + +--- + +## 📋 修复顺序建议 +1. 先修 🔴 B1~B3(崩溃/功能错误) +2. 再修 🟡 B4~B8(行为偏差) +3. 最后补 🟠 M1~M10(缺失功能) + +每次修改后验证编译 `scripts\build_debug.bat` 通过。 diff --git a/phobosvector.ini b/phobosvector.ini new file mode 100644 index 0000000000..294fbe9a66 --- /dev/null +++ b/phobosvector.ini @@ -0,0 +1,127 @@ + ================================================================ + Phobos Vector 标签一览 + [SomeVectorType] + Duration=N AE 持续时间 (帧) + Next=AnotherVectorType 串联下一个 Vector (过期后自动切换) + ================================================================ + + --- 通用 --- + Vector.TimeStep=1 每 N 帧运动一次 + Vector.DisabledFrames=0 首帧快照后冻结 N 帧 + Vector.SyncFacing=yes 抛射体面朝运动方向 / 单位转动 + Vector.OriginIsOnWorld=no yes=世界坐标系 FLH (朝北) + Vector.OriginIsOnBody=no yes=取车身 PrimaryFacing 无视炮塔 + Vector.Origin=Self Self | Launcher | Target + Vector.OriginFLH=0,0,0 Origin 偏移 + Vector.OriginNoUpdate=no yes=锁定初始坐标不刷新 + Vector.Force=yes yes=SetLocation 硬控 + Vector.Freeze=no yes=冻结原地 + Vector.AllowedTilt=no yes=允许坐标系倾斜 + + --- NormalVector (圆法向量, FLH 坐标系) --- + Vector.NormalVector=F,L,H 法向量 F/L/H 分量 + Vector.NormalRandomF.Min=0 F 分量随机下限 + Vector.NormalRandomF.Max=0 F 分量随机上限 + Vector.NormalRandomL.Min=0 + Vector.NormalRandomL.Max=0 + Vector.NormalRandomH.Min=0 + Vector.NormalRandomH.Max=0 + Vector.NormalFAnglePerStep=0 绕 F 轴角速度 (°/step) + Vector.NormalLAnglePerStep=0 + Vector.NormalHAnglePerStep=0 + Vector.NormalFAngleRanges.Min=0 绕 F 轴角速度 4 值区间 + Vector.NormalFAngleRanges.Max=0 + Vector.NormalFAngleRanges.Min2=0 + Vector.NormalFAngleRanges.Max2=0 + (同理 .NormalLAngleRanges / .NormalHAngleRanges) + + --- MoveTo 模式 --- + Vector.MoveTo=X,Y,Z 纯 FLH 位移 / step + Vector.GrowRate=X,Y,Z MoveTo 每帧增量 + Vector.AnglePerStep=0 MoveTo 自旋角度 (°/step) + + --- Circle 模式 --- + Vector.CircleRadius=-1 半径 (-1=取当前 XY 距离) + Vector.CircleSpeed=0 线速度 + Vector.CircleSpeedAcceleration=0 线速度加速度 + Vector.CircleMaxSpeed=0 线速度上限 (0=不限) + Vector.CircleMinSpeed=0 + Vector.CircleAnglePerStep=0 角速度 (°/step) + Vector.CircleAngleAcceleration=0 角速度加速度 + Vector.CircleMaxAngle=0 角速度上限 (0=不限) + Vector.CircleMinAngle=0 + Vector.CircleOrigin=F,L,H 圆心偏移 (默认世界坐标, AllowOriginTilt 时 FLH 旋转) + Vector.AllowOriginTilt=0 yes=圆心偏移跟随倾斜 + Vector.CircleRadiusGrow=0 半径每帧增长 (正=外扩) + Vector.CircleMaxRadius=0 半径上限 + Vector.CircleMinRadius=0 半径下限 + Vector.CircleEndOnMaxRadius=no 到达上限时结束 + Vector.CircleEndOnMinRadius=no + Vector.CircleRandomRadius.Min=0 随机半径范围 + Vector.CircleRandomRadius.Max=0 + Vector.CircleRandomAngle.Min=0 随机角速度范围 + Vector.CircleRandomAngle.Max=0 + + --- Origin 圆心移动 (Circle 模式专用) --- + Vector.Origin.MoveTo=F,L,H 圆心 FLH 位移 + Vector.Origin.GrowRate=F,L,H 每帧增量 + Vector.Origin.AnglePerStep=0 自旋角度 + Vector.Origin.TargetFLH=F,L,H 追踪目标 FLH + Vector.Origin.InitialSpeed=-1 初始速度 (-1=读单位 Speed) + Vector.Origin.Acceleration=0 加速度 + Vector.Origin.MaxSpeed=-1 (-1=不限) + Vector.Origin.MinSpeed=-1 + Vector.Origin.ReachTarget=no 到达模式 + Vector.Origin.ArcHeight=0 到达模式弧高 + Vector.Origin.CircleRadius=-1 + Vector.Origin.CircleSpeed=0 + Vector.Origin.CircleAnglePerStep=0 + Vector.Origin.CircleRadiusGrow=0 + Vector.Origin.CircleMaxRadius=0 + Vector.Origin.CircleMinRadius=0 + Vector.Origin.CircleEndOnMaxRadius=no + Vector.Origin.CircleEndOnMinRadius=no + Vector.Origin.NormalVector=F,L,H 圆心法向量 + Vector.Origin.NormalFAnglePerStep=0 + Vector.Origin.NormalLAnglePerStep=0 + Vector.Origin.NormalHAnglePerStep=0 + Vector.Origin.AllowedTilt=no + Vector.Origin.CircleOffset=X,Y,Z 原点世界偏移 + Vector.Origin.Origin=Self 参考系 Self|Launcher|Target|Source + Vector.Origin.OriginFLH=F,L,H + Vector.Origin .NormalFAngleRanges.Min/Min2 等同上 + + --- Speed / ReachTarget 模式 --- + Vector.TargetFLH=F,L,H 目标 FLH 偏移 + Vector.TargetOffsetF.Min=0 F 方向随机偏移范围 + Vector.TargetOffsetF.Max=0 + Vector.TargetOffsetL.Min=0 + Vector.TargetOffsetL.Max=0 + Vector.TargetOffsetH.Min=0 + Vector.TargetOffsetH.Max=0 + Vector.InitialSpeed=-1 初始速度 (-1=读单位 Speed) + Vector.RandomSpeed.Min=0 随机速度范围 + Vector.RandomSpeed.Max=0 + Vector.MaxSpeed=-1 (-1=不限) + Vector.MinSpeed=-1 + Vector.Acceleration=0 加速度 (每帧) + Vector.ReachTarget=no yes=剩余帧数强制到达 + Vector.ReachTargetEarlyEnd=0 提前 N 帧结束 (触发 Next) + + --- 弧高 (ReachTarget 模式) --- + Vector.ArcHeight=0 弧高 (lepton), 0=直线 + Vector.ArcRandomHeight.Min=0 随机弧高范围 + Vector.ArcRandomHeight.Max=0 + Vector.ArcPeakPercent=50 弧顶 Duration 百分比 (0-100) + Vector.ArcPeakRandomPercent=0 固定随机峰位 + Vector.ArcPeakRandomPercent.Min=0 随机峰位范围 + Vector.ArcPeakRandomPercent.Max=0 + Vector.ArcRotation=0 弧面旋转角 (°) + Vector.ArcRandomRotation.Min=0 + Vector.ArcRandomRotation.Max=0 + + --- 杂项 --- + Vector.AllowFallingDestroy=no 结束时摔死 + Vector.FallingDestroyHeight=208 摔死高度阈值 + Vector.AffectTechno=yes + Vector.AffectBullets=no diff --git a/src/Ext/Bullet/Body.cpp b/src/Ext/Bullet/Body.cpp index ad16905d82..c46a99c460 100644 --- a/src/Ext/Bullet/Body.cpp +++ b/src/Ext/Bullet/Body.cpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include BulletExt::ExtContainer BulletExt::ExtMap; @@ -487,8 +489,49 @@ void BulletExt::ExtData::Serialize(T& Stm) .Process(this->ParabombFallRate) .Process(this->IsInstantDetonation) .Process(this->FirepowerMult) - - .Process(this->Trajectory) // Keep this shit at last + .Process(this->VectorType) + .Process(this->Vector.Initialized) + .Process(this->Vector.CurrentFrame) + .Process(this->Vector.DisabledTimer) + .Process(this->Vector.CurrentSpeed) + .Process(this->Vector.CurrentAngle) + .Process(this->Vector.CurrentCircleRadius) + .Process(this->Vector.CurrentCircleSpeed) + .Process(this->Vector.CurrentCircleAngle) + .Process(this->Vector.InitialOriginPos) + .Process(this->Vector.InitialLocation) + .Process(this->Vector.PrevCirclePos) + .Process(this->Vector.ArcHeight) + .Process(this->Vector.ArcRotation) + .Process(this->Vector.ArcPeakPercent) + .Process(this->Vector.TargetOffset) + .Process(this->Vector.NormalRotF) + .Process(this->Vector.NormalRotL) + .Process(this->Vector.NormalRotH) + .Process(this->Vector.NormalStepF) + .Process(this->Vector.NormalStepL) + .Process(this->Vector.NormalStepH) + .Process(this->Vector.MovementFrames) + .Process(this->Vector.FacingRad) + .Process(this->Vector.TiltRad) + .Process(this->Vector.OriginOffset) + .Process(this->Vector.PrevCircleCenter) + .Process(this->Vector.OriginElapsed) + .Process(this->Vector.OriginSpeed) + .Process(this->Vector.OriginAngle) + .Process(this->Vector.OriginTargetOffset) + .Process(this->Vector.OriginCircleRadiusRuntime) + .Process(this->Vector.OriginCircleSpeedRuntime) + .Process(this->Vector.OriginCircleAngleRuntime) + .Process(this->Vector.OriginNormalRotFRuntime) + .Process(this->Vector.OriginNormalRotLRuntime) + .Process(this->Vector.OriginNormalRotHRuntime) + .Process(this->Vector.OriginNormalStepF) + .Process(this->Vector.OriginNormalStepL) + .Process(this->Vector.OriginNormalStepH) + .Process(this->Vector.OriginFacing) + .Process(this->Vector.OriginTilt) + .Process(this->Trajectory) ; } @@ -511,6 +554,11 @@ BulletExt::ExtContainer::ExtContainer() : Container("BulletClass") { } BulletExt::ExtContainer::~ExtContainer() = default; +void BulletExt::ExtData::VectorAI() +{ + VectorAI_Run(this->OwnerObject(), this->VectorType, this->Vector, this->OwnerObject() ? static_cast(this->OwnerObject())->Owner : nullptr, true); +} + // ============================= // container hooks diff --git a/src/Ext/Bullet/Body.h b/src/Ext/Bullet/Body.h index 2fd695624d..c2bbb17b63 100644 --- a/src/Ext/Bullet/Body.h +++ b/src/Ext/Bullet/Body.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include struct RadialFireStruct { @@ -38,6 +40,11 @@ class BulletExt TrajectoryPointer Trajectory; + AttachEffectTypeClass* VectorType; + VectorState Vector; + + void VectorAI(); + ExtData(BulletClass* OwnerObject) : Extension(OwnerObject) , TypeExtData { nullptr } , FirerHouse { nullptr } @@ -47,6 +54,8 @@ class BulletExt , DetonateOnInterception { true } , LaserTrails {} , Trajectory { nullptr } + , VectorType { nullptr } + , Vector {} , SnappedToTarget { false } , DamageNumberOffset { INT32_MIN } , ParabombFallRate { 0 } diff --git a/src/Ext/Bullet/Hooks.DetonateLogics.cpp b/src/Ext/Bullet/Hooks.DetonateLogics.cpp index e4b1f505ba..e7dab77c6d 100644 --- a/src/Ext/Bullet/Hooks.DetonateLogics.cpp +++ b/src/Ext/Bullet/Hooks.DetonateLogics.cpp @@ -83,6 +83,11 @@ DEFINE_HOOK(0x4690C1, BulletClass_Logics_DetonateOnAllMapObjects, 0x8) GET(BulletClass*, pThis, ESI); + // Debug::Log("[VEC] F=%d EXPLODE POS=(%d,%d,%d) TGT=(%d,%d,%d)\n", + // Unsorted::CurrentFrame, + // pThis->Location.X, pThis->Location.Y, pThis->Location.Z, + // pThis->TargetCoords.X, pThis->TargetCoords.Y, pThis->TargetCoords.Z); + auto const pWHExt = WarheadTypeExt::ExtMap.Find(pThis->WH); if (pWHExt->DetonateOnAllMapObjects && !pWHExt->WasDetonatedOnAllMapObjects diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index bf55bf24ab..397a521291 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -2,8 +2,21 @@ #include #include #include +#include #include +//static void VecLog(const char* fmt, ...) +//{ +// va_list args; +// va_start(args, fmt); +// FILE* f = fopen("vec_debug.log", "a"); +// if (f) { +// vfprintf(f, fmt, args); +// fclose(f); +// } +// va_end(args); +//} + // has everything inited except SpawnNextAnim at this point DEFINE_HOOK(0x466556, BulletClass_Init, 0x6) { @@ -15,6 +28,19 @@ DEFINE_HOOK(0x466556, BulletClass_Init, 0x6) { pExt->FirerHouse = pThis->Owner->Owner; pExt->FirepowerMult = TechnoExt::GetCurrentFirepowerMultiplier(pThis->Owner); + + // Init Vector from firing techno's AE types + auto const pTechnoExt = TechnoExt::ExtMap.Find(pThis->Owner); + for (auto const& ae : pTechnoExt->AttachedEffects) + { + auto const pAEType = ae->GetType(); + if (!pAEType) continue; + if (pAEType->HasVector() && pAEType->Vector_AffectBullets) + { + pExt->VectorType = pAEType; + break; + } + } } auto const pType = pThis->Type; @@ -44,6 +70,32 @@ DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6) BulletAITemp::ExtData = pBulletExt; BulletAITemp::TypeExtData = pBulletTypeExt; + if (pBulletExt->VectorType) + { + int earlyEnd = pBulletExt->VectorType->Vector_ReachTargetEarlyEnd; + int endFrame = pBulletExt->VectorType->Duration - earlyEnd; + if (endFrame < 1) endFrame = pBulletExt->VectorType->Duration; + if (pBulletExt->Vector.CurrentFrame >= endFrame || pBulletExt->Vector.CurrentFrame > pBulletExt->VectorType->Duration) + { + bool foundNext = false; + for (auto pNextType : pBulletExt->VectorType->Next) + { + if (pNextType && pNextType->HasVector() && pNextType->Vector_AffectBullets) + { + pBulletExt->VectorType = pNextType; + pBulletExt->Vector = {}; + foundNext = true; + break; + } + } + if (!foundNext) + pBulletExt->VectorType = nullptr; + } + else { + pBulletExt->VectorAI(); + } + } + if (pBulletExt->InterceptedStatus & InterceptedStatus::Targeted) { if (const auto pTarget = abstract_cast(pThis->Target)) @@ -81,9 +133,8 @@ DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6) } } - //Because the laser trails will be drawn before the calculation of changing the velocity direction in each frame. - //This will cause the laser trails to be drawn in the wrong position too early, resulting in a visual appearance resembling a "bouncing". - //Let trajectories draw their own laser trails after the Trajectory's OnAI() to avoid predicting incorrect positions or pass through targets. + // VectorAI moved to BulletClass_AI_Trailer for correct engine-movement-final override + if (!pBulletExt->Trajectory && pBulletExt->LaserTrails.size()) { const CoordStruct location = pThis->GetCoords(); @@ -141,6 +192,53 @@ DEFINE_HOOK(0x466897, BulletClass_AI_Trailer, 0x6) return SkipGameCode; } +DEFINE_HOOK_AGAIN(0x467FEE, BulletClass_AI_UpdateEnd, 0x6) +DEFINE_HOOK(0x466781, BulletClass_AI_UpdateEnd, 0x6) +{ + GET(BulletClass*, pThis, EBP); + auto const pExt = BulletExt::ExtMap.Find(pThis); + + // Debug::Log("[VEC] F=%d UE STORED=(%d,%d,%d) POS=(%d,%d,%d) VEL=(%.0f,%.0f,%.0f) SRC=(%d,%d,%d) TGT=(%d,%d,%d)\n", + // Unsorted::CurrentFrame, + // pExt ? pExt->Vector.StoredDisp.X : -1, + // pExt ? pExt->Vector.StoredDisp.Y : -1, + // pExt ? pExt->Vector.StoredDisp.Z : -1, + // pThis->Location.X, pThis->Location.Y, pThis->Location.Z, + // pThis->Velocity.X, pThis->Velocity.Y, pThis->Velocity.Z, + // pThis->SourceCoords.X, pThis->SourceCoords.Y, pThis->SourceCoords.Z, + // pThis->TargetCoords.X, pThis->TargetCoords.Y, pThis->TargetCoords.Z); + + if (pExt && pExt->VectorType) + { + auto& pos = pExt->Vector.StoredDisp; + if (pos.X != 0 || pos.Y != 0 || pos.Z != 0) + { + CoordStruct prev = pThis->GetCoords(); + //VecLog("[VEC] FRAME=%d APPLY_PREV=(%d,%d,%d) STORED=(%d,%d,%d) DELTA=(%d,%d,%d)\n", + // Unsorted::CurrentFrame, + // prev.X, prev.Y, prev.Z, + // pos.X, pos.Y, pos.Z, + // pos.X - prev.X, pos.Y - prev.Y, pos.Z - prev.Z); + pThis->SetLocation(pos); + pThis->SourceCoords = pos; + if (pExt->VectorType->Vector_SyncFacing) + { + double dx = (double)(pos.X - prev.X); + double dy = (double)(pos.Y - prev.Y); + double dz = (double)(pos.Z - prev.Z); + double mag = std::sqrt(dx * dx + dy * dy + dz * dz); + if (mag > 1e-6) { dx /= mag; dy /= mag; dz /= mag; } + pThis->Velocity.X = dx; + pThis->Velocity.Y = dy; + pThis->Velocity.Z = dz; + } + pos = CoordStruct::Empty; + } + } + + return 0; +} + // Inviso bullets behave differently in BulletClass::AI when their target is bullet and // seemingly (at least partially) adopt characteristics of a vertical projectile. // This is a potentially slightly hacky solution to that, as proper solution @@ -470,6 +568,9 @@ DEFINE_HOOK(0x468D3F, BulletClass_ShouldExplode_AirTarget, 0x6) auto const pExt = BulletExt::ExtMap.Find(pThis); + if (pExt->VectorType) + return SkipCheck; + if (pExt->Trajectory && CheckTrajectoryCanNotAlwaysSnap(pExt->Trajectory->Flag())) return SkipCheck; diff --git a/src/Ext/Techno/Body.Update.cpp b/src/Ext/Techno/Body.Update.cpp index 8b8ed569bd..09d09e720a 100644 --- a/src/Ext/Techno/Body.Update.cpp +++ b/src/Ext/Techno/Body.Update.cpp @@ -1944,6 +1944,20 @@ void TechnoExt::ExtData::UpdateAttachEffects() } } + // Next AE chain + if (hasExpired && !pType->Next.empty()) + { + auto const pInvoker = attachEffect->GetInvoker(); + auto const pInvokerHouse = attachEffect->InvokerHouse; + for (auto pNextType : pType->Next) + { + if (pNextType) + AttachEffectClass::CreateAndAttach(pNextType, pThis, pThis->GetTechnoType(), this->AttachedEffects, + pInvokerHouse ? pInvokerHouse : pThis->Owner, pInvoker ? pInvoker : pThis, attachEffect->Source, AEAttachParams{}); + } + altered = true; + } + if (shouldDiscard && attachEffect->ResetIfRecreatable()) { ++it; diff --git a/src/New/Entity/AttachEffectClass.cpp b/src/New/Entity/AttachEffectClass.cpp index 15716182ac..8b80d81e0f 100644 --- a/src/New/Entity/AttachEffectClass.cpp +++ b/src/New/Entity/AttachEffectClass.cpp @@ -17,6 +17,7 @@ AttachEffectClass::AttachEffectClass() , NeedsRecalculateStat { false } , LastDiscardCheckFrame { -1 } , LastDiscardCheckValue { false } + , Vector { } { this->HasInitialized = false; AttachEffectClass::Array.emplace_back(this); @@ -257,6 +258,7 @@ void AttachEffectClass::AI() this->CloakCheck(); this->OnlineCheck(); this->AnimCheck(); + this->VectorAI(); } void AttachEffectClass::AI_Temporal() @@ -1059,6 +1061,23 @@ void AttachEffectClass::TransferAttachedEffects(TechnoClass* pSource, TechnoClas #pragma endregion +CoordStruct AttachEffectClass::GetFLHAbsoluteCoords(CoordStruct origin, CoordStruct flh, DirStruct facing) +{ + double rad = facing.GetRadian<32>(); + double cosR = std::cos(rad); + double sinR = std::sin(rad); + return { origin.X + static_cast(flh.X * cosR + flh.Y * sinR), + origin.Y + static_cast(flh.X * sinR - flh.Y * cosR), + origin.Z + flh.Z }; +} + +void AttachEffectClass::VectorAI() +{ + if (!this->Type->Vector_AffectTechno) + return; + VectorAI_Run(this->Techno, this->Type, this->Vector, this->Invoker, false); +} + // ============================= // load / save @@ -1092,6 +1111,47 @@ bool AttachEffectClass::Serialize(T& Stm) .Process(this->LastActiveStat) .Process(this->LaserTrail) .Process(this->NeedsRecalculateStat) + .Process(this->Vector.Initialized) + .Process(this->Vector.CurrentFrame) + .Process(this->Vector.DisabledTimer) + .Process(this->Vector.CurrentSpeed) + .Process(this->Vector.CurrentAngle) + .Process(this->Vector.CurrentCircleRadius) + .Process(this->Vector.CurrentCircleSpeed) + .Process(this->Vector.CurrentCircleAngle) + .Process(this->Vector.InitialOriginPos) + .Process(this->Vector.InitialLocation) + .Process(this->Vector.PrevCirclePos) + .Process(this->Vector.ArcHeight) + .Process(this->Vector.ArcRotation) + .Process(this->Vector.ArcPeakPercent) + .Process(this->Vector.TargetOffset) + .Process(this->Vector.NormalRotF) + .Process(this->Vector.NormalRotL) + .Process(this->Vector.NormalRotH) + .Process(this->Vector.NormalStepF) + .Process(this->Vector.NormalStepL) + .Process(this->Vector.NormalStepH) + .Process(this->Vector.MovementFrames) + .Process(this->Vector.FacingRad) + .Process(this->Vector.TiltRad) + .Process(this->Vector.OriginOffset) + .Process(this->Vector.PrevCircleCenter) + .Process(this->Vector.OriginElapsed) + .Process(this->Vector.OriginSpeed) + .Process(this->Vector.OriginAngle) + .Process(this->Vector.OriginTargetOffset) + .Process(this->Vector.OriginCircleRadiusRuntime) + .Process(this->Vector.OriginCircleSpeedRuntime) + .Process(this->Vector.OriginCircleAngleRuntime) + .Process(this->Vector.OriginNormalRotFRuntime) + .Process(this->Vector.OriginNormalRotLRuntime) + .Process(this->Vector.OriginNormalRotHRuntime) + .Process(this->Vector.OriginNormalStepF) + .Process(this->Vector.OriginNormalStepL) + .Process(this->Vector.OriginNormalStepH) + .Process(this->Vector.OriginFacing) + .Process(this->Vector.OriginTilt) .Success(); } diff --git a/src/New/Entity/AttachEffectClass.h b/src/New/Entity/AttachEffectClass.h index c596d6d709..50f55b8a84 100644 --- a/src/New/Entity/AttachEffectClass.h +++ b/src/New/Entity/AttachEffectClass.h @@ -1,6 +1,7 @@ #pragma once #include +#include class LaserTrailClass; @@ -22,6 +23,7 @@ class AttachEffectClass void CreateAnim(); void UpdateCumulativeAnim(); void TransferCumulativeAnim(AttachEffectClass* pSource); + void VectorAI(); bool CanShowAnim() const { @@ -58,15 +60,16 @@ class AttachEffectClass static int Detach(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo); static int DetachByGroups(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo); static void TransferAttachedEffects(TechnoClass* pSource, TechnoClass* pTarget); + static CoordStruct GetFLHAbsoluteCoords(CoordStruct origin, CoordStruct flh, DirStruct facing); + + static AttachEffectClass* CreateAndAttach(AttachEffectTypeClass* pType, TechnoClass* pTarget, TechnoTypeClass* pTargetType, std::vector>& targetAEs, HouseClass* pInvokerHouse, TechnoClass* pInvoker, + AbstractClass* pSource, AEAttachParams const& attachInfo, bool checkCumulative = true); private: void OnlineCheck(); void CloakCheck(); void AnimCheck(); - static AttachEffectClass* CreateAndAttach(AttachEffectTypeClass* pType, TechnoClass* pTarget, TechnoTypeClass* pTargetType, std::vector>& targetAEs, HouseClass* pInvokerHouse, TechnoClass* pInvoker, - AbstractClass* pSource, AEAttachParams const& attachInfo, bool checkCumulative = true); - static int DetachTypes(TechnoClass* pTarget, AEAttachInfoTypeClass const& attachEffectInfo, std::vector const& types); static int RemoveAllOfType(AttachEffectTypeClass* pType, TechnoClass* pTarget, int minCount, int maxCount); @@ -81,9 +84,13 @@ class AttachEffectClass int RecreationDelay; AttachEffectTypeClass* Type; TechnoClass* Techno; + +public: HouseClass* InvokerHouse; TechnoClass* Invoker; AbstractClass* Source; + +private: AnimClass* Animation; bool IsAnimHidden; bool IsInTunnel; @@ -101,6 +108,8 @@ class AttachEffectClass bool HasCumulativeAnim; bool ShouldBeDiscarded; bool NeedsRecalculateStat; + + VectorState Vector; }; // Container for TechnoClass-specific AttachEffect fields. diff --git a/src/New/Entity/VectorState.cpp b/src/New/Entity/VectorState.cpp new file mode 100644 index 0000000000..9ab91a2548 --- /dev/null +++ b/src/New/Entity/VectorState.cpp @@ -0,0 +1,989 @@ +#include "VectorState.h" +#include "AttachEffectClass.h" +#include +#include +#include + +//static void VecLog(const char* fmt, ...) +//{ +// va_list args; +// va_start(args, fmt); +// FILE* f = fopen("vec_debug.log", "a"); +// if (f) { +// vfprintf(f, fmt, args); +// fclose(f); +// } +// va_end(args); +//} + +static double V_Random(double min, double max) +{ + 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 (pObject->WhatAmI() == AbstractType::Building) + 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; + + auto EffectiveOriginNoUpdate = [&]() -> bool { + if (pType->Vector_OriginNoUpdate.isset()) + return pType->Vector_OriginNoUpdate.Get(); + bool hasCircle = pType->Vector_CircleRadius > 0 || pType->Vector_CircleSpeed != 0 || pType->Vector_CircleAnglePerStep > 0.0 + || (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + || (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin); + bool hasTargetMode = pType->Vector_TargetFLH.isset() || pType->Vector_ReachTarget; + bool hasMoveTo = pType->Vector_MoveTo != CoordStruct::Empty; + return (hasTargetMode || hasMoveTo) && !hasCircle; + }; + + s.MovementFrames++; + s.NormalRotF += s.NormalStepF; + s.NormalRotL += s.NormalStepL; + s.NormalRotH += s.NormalStepH; + + auto GetPos = [&]() { return pObject->GetCoords(); }; + + if (!s.Initialized) + { + s.Initialized = true; + s.InitialOriginPos = GetPos(); + switch (pType->Vector_Origin) + { + case VectorOrigin::Launcher: + if (pInvoker) s.InitialOriginPos = pInvoker->GetCoords(); + break; + case VectorOrigin::Target: + if (isBullet) + { + auto const pB = static_cast(pObject); + s.InitialOriginPos = pB->TargetCoords; + } + break; + } + s.InitialLocation = GetPos(); + + if (pType->Vector_NormalVector.isset() + || (pType->Vector_NormalRandomFMax > pType->Vector_NormalRandomFMin) + || (pType->Vector_NormalRandomLMax > pType->Vector_NormalRandomLMin) + || (pType->Vector_NormalRandomHMax > pType->Vector_NormalRandomHMin)) + { + CoordStruct nv = pType->Vector_NormalVector.isset() ? pType->Vector_NormalVector.Get() : CoordStruct{}; + if (pType->Vector_NormalRandomFMax > pType->Vector_NormalRandomFMin) + nv.X = V_Random(pType->Vector_NormalRandomFMin, pType->Vector_NormalRandomFMax); + if (pType->Vector_NormalRandomLMax > pType->Vector_NormalRandomLMin) + nv.Y = V_Random(pType->Vector_NormalRandomLMin, pType->Vector_NormalRandomLMax); + if (pType->Vector_NormalRandomHMax > pType->Vector_NormalRandomHMin) + nv.Z = V_Random(pType->Vector_NormalRandomHMin, pType->Vector_NormalRandomHMax); + double lenXY = std::sqrt((double)nv.X * nv.X + nv.Y * nv.Y); + s.FacingRad = lenXY > 1e-6 ? std::atan2(nv.X, nv.Y) : 0.0; + s.TiltRad = lenXY > 1e-6 ? std::atan2(nv.Z, lenXY) : (nv.Z > 0 ? 3.1415926535 / 2.0 : -3.1415926535 / 2.0); + } + else + { + switch (pType->Vector_Origin) + { + case VectorOrigin::Launcher: + if (pInvoker) + { + auto const pLT = static_cast(pInvoker); + s.FacingRad = pLT->TurretFacing().GetRadian<32>(); + } + break; + case VectorOrigin::Target: + { + CoordStruct selfPos = GetPos(); + CoordStruct targetPos = selfPos; + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) + targetPos = pB->Target->GetCoords(); + else + targetPos = pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) + targetPos = pT->Target->GetCoords(); + } + double dx = selfPos.X - targetPos.X; + double dy = selfPos.Y - targetPos.Y; + double dz = selfPos.Z - targetPos.Z; + s.FacingRad = std::atan2(dy, dx); + double lenXY = std::sqrt(dx * dx + dy * dy); + s.TiltRad = (lenXY > 1e-6 && pType->Vector_AllowedTilt) ? std::atan2(dz, lenXY) : 0.0; + break; + } + default: + if (isBullet) + { + auto const pB = static_cast(pObject); + double vx = pB->Velocity.X, vy = pB->Velocity.Y; + s.FacingRad = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + } + else + { + auto const pT = static_cast(pObject); + s.FacingRad = pT->TurretFacing().GetRadian<32>(); + } + s.TiltRad = 0.0; + break; + } + } + s.OriginFacing = s.FacingRad; + s.OriginTilt = s.TiltRad; + + double speed; + if (pType->Vector_InitialSpeed >= 0) + speed = static_cast(pType->Vector_InitialSpeed); + else if (isBullet) + speed = static_cast(static_cast(pObject)->Speed); + else + { + auto const pT = static_cast(pObject); + TechnoTypeClass* pTT = pT->GetTechnoType(); + speed = pTT->JumpjetSpeed > 0 ? static_cast(pTT->JumpjetSpeed) : static_cast(pTT->Speed); + } + if (pType->Vector_RandomSpeedMin != pType->Vector_RandomSpeedMax) + speed = V_Random(pType->Vector_RandomSpeedMin, pType->Vector_RandomSpeedMax); + s.CurrentSpeed = speed; + + 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_ArcPeakRandomPercentMin > 0 && pType->Vector_ArcPeakRandomPercentMax > pType->Vector_ArcPeakRandomPercentMin) + s.ArcPeakPercent = V_Random(pType->Vector_ArcPeakRandomPercentMin, pType->Vector_ArcPeakRandomPercentMax) / 100.0; + else if (pType->Vector_ArcPeakRandomPercent > 0) + s.ArcPeakPercent = pType->Vector_ArcPeakRandomPercent / 100.0; + else + s.ArcPeakPercent = pType->Vector_ArcPeakPercent / 100.0; + if (s.ArcPeakPercent <= 0.0) s.ArcPeakPercent = 0.5; + if (s.ArcPeakPercent >= 1.0) s.ArcPeakPercent = 0.5; + + if (pType->Vector_TargetFLH.isset()) + { + CoordStruct offset = CoordStruct::Empty; + if (pType->Vector_TargetOffsetFMin != pType->Vector_TargetOffsetFMax) + offset.X = pType->Vector_TargetOffsetFMin + rand() % (pType->Vector_TargetOffsetFMax - pType->Vector_TargetOffsetFMin + 1); + if (pType->Vector_TargetOffsetLMin != pType->Vector_TargetOffsetLMax) + 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() + || (pType->Vector_NormalRandomFMax > pType->Vector_NormalRandomFMin) + || (pType->Vector_NormalRandomLMax > pType->Vector_NormalRandomLMin) + || (pType->Vector_NormalRandomHMax > pType->Vector_NormalRandomHMin); + if (! EffectiveOriginNoUpdate() && !hasNormal && !pType->Vector_OriginIsOnWorld) + { + switch (pType->Vector_Origin) + { + 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 + targetPos = pB->TargetCoords; + } + 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 && pInvoker->IsAlive) + { + 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 ( EffectiveOriginNoUpdate()) + 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 = EffectiveOriginNoUpdate() ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Source: + originPos = EffectiveOriginNoUpdate() ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Self: + originPos = EffectiveOriginNoUpdate() ? 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) + || (pType->Vector_CircleRandomAngleMax2 > pType->Vector_CircleRandomAngleMin2); + + 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 = V_Random(static_cast(pType->Vector_CircleRandomRadiusMin), static_cast(pType->Vector_CircleRandomRadiusMax)); + } + s.CurrentCircleSpeed += pType->Vector_CircleSpeedAcceleration; + if (pType->Vector_CircleMaxSpeed != 0 && s.CurrentCircleSpeed > pType->Vector_CircleMaxSpeed) + 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) + { + bool useSecond = pType->Vector_CircleRandomAngleMax2 > pType->Vector_CircleRandomAngleMin2 + && (std::rand() % 2); + if (useSecond) + s.CurrentCircleAngle = V_Random(pType->Vector_CircleRandomAngleMin2, pType->Vector_CircleRandomAngleMax2); + else + s.CurrentCircleAngle = V_Random(pType->Vector_CircleRandomAngleMin, pType->Vector_CircleRandomAngleMax); + } + } + s.CurrentCircleAngle += pType->Vector_CircleAngleAcceleration; + if (pType->Vector_CircleMaxAngle != 0 && s.CurrentCircleAngle > pType->Vector_CircleMaxAngle) + 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) + { + //VecLog("[ORIGIN] Frame=%d MOVFRM=%d CR=%d CSP=%d CAng=%.1f\n", + // Unsorted::CurrentFrame, s.MovementFrames, + // (int)pType->Vector_OriginCircleRadius, (int)pType->Vector_OriginCircleSpeed, + // pType->Vector_OriginCircleAnglePerStep); + CoordStruct baseCenter = originPos; + + if (pType->Vector_OriginOrigin != VectorOrigin::Self) + { + 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 (pType->Vector_OriginCircleOffset.isset()) + { + baseCenter.X += pType->Vector_OriginCircleOffset.Get().X; + baseCenter.Y += pType->Vector_OriginCircleOffset.Get().Y; + baseCenter.Z += pType->Vector_OriginCircleOffset.Get().Z; + } + + if (s.MovementFrames == 1) + { + s.OriginOffset = { circleCenter.X - baseCenter.X, circleCenter.Y - baseCenter.Y, circleCenter.Z - baseCenter.Z }; + s.OriginCircleRadiusRuntime = static_cast(pType->Vector_OriginCircleRadius); + s.OriginCircleSpeedRuntime = static_cast(pType->Vector_OriginCircleSpeed); + s.OriginCircleAngleRuntime = 0.0; + { + int ofx = pType->Vector_OriginTargetOffsetFMax > pType->Vector_OriginTargetOffsetFMin + ? static_cast(V_Random(static_cast(pType->Vector_OriginTargetOffsetFMin), static_cast(pType->Vector_OriginTargetOffsetFMax))) : 0; + int ofy = pType->Vector_OriginTargetOffsetLMax > pType->Vector_OriginTargetOffsetLMin + ? static_cast(V_Random(static_cast(pType->Vector_OriginTargetOffsetLMin), static_cast(pType->Vector_OriginTargetOffsetLMax))) : 0; + int ofz = pType->Vector_OriginTargetOffsetHMax > pType->Vector_OriginTargetOffsetHMin + ? static_cast(V_Random(static_cast(pType->Vector_OriginTargetOffsetHMin), static_cast(pType->Vector_OriginTargetOffsetHMax))) : 0; + s.OriginTargetOffset = { ofx, ofy, ofz }; + } + + if (pType->Vector_OriginNormalVector.isset()) + { + CoordStruct nv = pType->Vector_OriginNormalVector.Get(); + double len = std::sqrt(static_cast(nv.X * nv.X + nv.Y * nv.Y)); + s.OriginFacing = len > 1e-6 ? std::atan2(static_cast(nv.X), static_cast(nv.Y)) : 0; + s.OriginTilt = len > 1e-6 ? std::atan2(static_cast(nv.Z), len) : (nv.Z > 0 ? 3.14159265358979323846 / 2.0 : -3.14159265358979323846 / 2.0); + // 哨兵:NormalVector 存在但未显式设 CircleRadius 时标记为 0 + if (pType->Vector_OriginCircleRadius < 0) + s.OriginCircleRadiusRuntime = 0.0; + } + else + { + // 默认水平圆面(法向量朝上) + s.OriginFacing = 0; + s.OriginTilt = 3.14159265358979323846 / 2.0; + } + } + + 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)); + + if (pType->Vector_OriginReachTarget) + { + int totalDuration = pType->Duration / pType->Vector_TimeStep; + if (totalDuration < 1) totalDuration = 1; + int effectiveDuration = totalDuration - pType->Vector_DisabledFrames; + if (effectiveDuration < 1) effectiveDuration = 1; + int effectiveSteps = effectiveDuration; + int rem = effectiveSteps - s.MovementFrames; + if (rem <= 0) + { + disp.X = targetWorld.X - originCenter.X; + disp.Y = targetWorld.Y - originCenter.Y; + disp.Z = targetWorld.Z - originCenter.Z; + s.OriginOffset.X += disp.X; s.OriginOffset.Y += disp.Y; s.OriginOffset.Z += disp.Z; + circleCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + s.PrevCircleCenter = circleCenter; + s.DisabledTimer = -1; // Deactivate-equivalent + s.OriginElapsed = 0; // no further OriginUpdate + } + else + { + disp.X = (targetWorld.X - originCenter.X) / rem; + disp.Y = (targetWorld.Y - originCenter.Y) / rem; + disp.Z = (targetWorld.Z - originCenter.Z) / rem; + if (pType->Vector_OriginArcHeight) + { + double t = effectiveSteps > 0 ? static_cast(s.OriginElapsed) / effectiveSteps : 0; + double t0 = effectiveSteps > 0 ? static_cast(s.OriginElapsed - 1) / effectiveSteps : 0; + disp.Z += static_cast(4.0 * pType->Vector_OriginArcHeight * (t * (1 - t) - t0 * (1 - t0))); + } + } + } + else + { + s.OriginSpeed += static_cast(pType->Vector_OriginAcceleration); + if (pType->Vector_OriginMaxSpeed >= 0 && s.OriginSpeed > pType->Vector_OriginMaxSpeed) + s.OriginSpeed = static_cast(pType->Vector_OriginMaxSpeed); + if (pType->Vector_OriginMinSpeed >= 0 && s.OriginSpeed < pType->Vector_OriginMinSpeed) + s.OriginSpeed = static_cast(pType->Vector_OriginMinSpeed); + + int dx = targetWorld.X - originCenter.X, dy = targetWorld.Y - originCenter.Y, dz = targetWorld.Z - originCenter.Z; + double dist = std::sqrt(static_cast(dx * dx + dy * dy + dz * dz)); + if (dist >= 1.0) + { + double sv = s.OriginSpeed / dist; + disp = { static_cast(dx * sv), static_cast(dy * sv), static_cast(dz * sv) }; + } + } + } + else + { + 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 = pType->Vector_OriginLissajous ? V_Deg2Rad(s.OriginCircleAngleRuntime) : V_Deg2Rad(stepO); + double ca = std::cos(r), sa = std::sin(r); + double dxO = static_cast(s.OriginOffset.X); + double dyO = static_cast(s.OriginOffset.Y); + 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 }; + //VecLog("[ORIGIN] of=%.3f ot=%.3f rad=%.1f ang=%.1f disp=(%d,%d,%d) offset=(%d,%d,%d) center=(%d,%d,%d)\n", + // oFacing, oTilt, s.OriginCircleRadiusRuntime, s.OriginCircleAngleRuntime, + // disp.X, disp.Y, disp.Z, + // s.OriginOffset.X, s.OriginOffset.Y, s.OriginOffset.Z, + // circleCenter.X, circleCenter.Y, circleCenter.Z); + s.OriginElapsed++; + } + + 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) + { + s.StoredDisp = { currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.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) + { + s.StoredDisp = { currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.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 }; + //VecLog("[VEC] Frame=%d FLH=(%d,%d,%d) TGTFLH=(%d,%d,%d) OFFSET=(%d,%d,%d) ORIGIN=%d ONWORLD=%d ONBODY=%d\n", + // Unsorted::CurrentFrame, + // pType->Vector_TargetFLH.Get().X, pType->Vector_TargetFLH.Get().Y, pType->Vector_TargetFLH.Get().Z, + // frameTargetFlh.X, frameTargetFlh.Y, frameTargetFlh.Z, + // s.TargetOffset.X, s.TargetOffset.Y, s.TargetOffset.Z, + // static_cast(pType->Vector_Origin.Get()), + // static_cast(pType->Vector_OriginIsOnWorld.Get()), + // static_cast(pType->Vector_OriginIsOnBody.Get())); + CoordStruct frameTarget; + + switch (pType->Vector_Origin) + { + 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 }; + + //VecLog("[VEC] CURR=(%d,%d,%d) FDIR=%.4f FACING=(%.4f,%.4f) FRMTGT=(%d,%d,%d) DIRVEC=(%d,%d,%d) DIRLEN=%.1f MOVFRM=%d\n", + // currentPos.X, currentPos.Y, currentPos.Z, + // mainFacingDir.GetRadian<32>(), + // effectiveFacing, effectiveTilt, + // frameTarget.X, frameTarget.Y, frameTarget.Z, + // dirVec.X, dirVec.Y, dirVec.Z, + // dirLen, + // s.MovementFrames); + + if (pType->Vector_ReachTarget) + { + int totalDuration = pType->Duration / pType->Vector_TimeStep; + if (totalDuration < 1) totalDuration = 1; + int effectiveDuration = totalDuration - pType->Vector_DisabledFrames; + if (effectiveDuration < 1) effectiveDuration = 1; + int remainingFrames = effectiveDuration - s.MovementFrames + 1; + + if (pType->Vector_ReachTargetEarlyEnd > 0 && pType->Vector_ReachTargetEarlyEnd < effectiveDuration + && remainingFrames <= pType->Vector_ReachTargetEarlyEnd) + { + s.DisabledTimer = -1; + s.MovementFrames = effectiveDuration + 1; // 退出 EarlyEnd 区间,下一步 remainingFrames <= 0 + } + + if (remainingFrames <= 0) + { + if (pType->Vector_Force) + { + resultDisp.X = frameTarget.X - currentPos.X; + resultDisp.Y = frameTarget.Y - currentPos.Y; + resultDisp.Z = frameTarget.Z - currentPos.Z; + } + } + else if (dirLen > 1e-6) + { + double adjustedSpeed = dirLen / remainingFrames; + resultDisp.X = static_cast(dirVec.X / dirLen * adjustedSpeed); + resultDisp.Y = static_cast(dirVec.Y / dirLen * adjustedSpeed); + resultDisp.Z = static_cast(dirVec.Z / dirLen * adjustedSpeed); + + if (s.ArcHeight != 0) + { + double t = static_cast(s.MovementFrames) / effectiveDuration; + double peakPct = s.ArcPeakPercent > 0 ? s.ArcPeakPercent : 0.5; + double arcOffset; + if (t <= peakPct) + { + double u = t / peakPct; + arcOffset = s.ArcHeight * u * (2.0 - u); + } + else + { + double u = (t - peakPct) / (1.0 - peakPct); + arcOffset = s.ArcHeight * (1.0 - u * u); + } + double baseX = s.InitialLocation.X + (frameTarget.X - s.InitialLocation.X) * t; + double baseY = s.InitialLocation.Y + (frameTarget.Y - s.InitialLocation.Y) * t; + double baseZ = s.InitialLocation.Z + (frameTarget.Z - s.InitialLocation.Z) * t; + + if (s.ArcRotation == 0.0) + { + resultDisp.X = static_cast(baseX - currentPos.X); + resultDisp.Y = static_cast(baseY - currentPos.Y); + resultDisp.Z = static_cast(baseZ + arcOffset - currentPos.Z); + } + 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(baseX + rx * arcOffset - currentPos.X); + resultDisp.Y = static_cast(baseY + ry * arcOffset - currentPos.Y); + resultDisp.Z = static_cast(baseZ + rz * arcOffset - currentPos.Z); + } + else + { + resultDisp.X = static_cast(baseX - currentPos.X); + resultDisp.Y = static_cast(baseY - currentPos.Y); + resultDisp.Z = static_cast(baseZ + arcOffset - currentPos.Z); + } + } + } + } + } + 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); + s.StoredDisp = { currentPos.X + resultDisp.X, currentPos.Y + resultDisp.Y, currentPos.Z + resultDisp.Z }; + //VecLog("[VEC] RESULT DISP=(%d,%d,%d) STORED=(%d,%d,%d) VEL=(%.1f,%.1f,%.1f)\n", + // resultDisp.X, resultDisp.Y, resultDisp.Z, + // s.StoredDisp.X, s.StoredDisp.Y, s.StoredDisp.Z, + // pB->Velocity.X, pB->Velocity.Y, pB->Velocity.Z); + } + else + { + pObject->SetLocation({ currentPos.X + resultDisp.X, currentPos.Y + resultDisp.Y, currentPos.Z + resultDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + + if (!isBullet && pType->Vector_SyncFacing && (resultDisp.X != 0 || resultDisp.Y != 0)) + { + double ang = std::atan2(static_cast(resultDisp.Y), static_cast(resultDisp.X)); + auto dir = DirStruct(static_cast(ang * 32768.0 / 3.14159265358979323846)); + static_cast(pObject)->PrimaryFacing.SetDesired(dir); + } + + if (pType->Vector_AllowFallingDestroy && pType->Vector_FallingDestroyHeight > 0) + { + if (s.InitialLocation.Z - GetPos().Z > pType->Vector_FallingDestroyHeight) + return; + } +} diff --git a/src/New/Entity/VectorState.cpp.circlebackup b/src/New/Entity/VectorState.cpp.circlebackup new file mode 100644 index 0000000000..74abef89a1 --- /dev/null +++ b/src/New/Entity/VectorState.cpp.circlebackup @@ -0,0 +1,757 @@ +#include "VectorState.h" +#include "AttachEffectClass.h" +#include +#include + +static double V_Random(double min, double max) +{ + if (min >= max) return min; + return min + (max - min) * static_cast(std::rand()) / RAND_MAX; +} + +static double V_Deg2Rad(double deg) { return deg * 3.14159265358979323846 / 180.0; } +static double V_Rad2Deg(double rad) { return rad * 180.0 / 3.14159265358979323846; } + +static DirStruct V_Radians2Dir(double rad) +{ + return DirStruct(static_cast(rad * 32768.0 / 3.14159265358979323846)); +} + +static DirStruct V_Point2Dir(CoordStruct from, CoordStruct to) +{ + double dx = to.X - from.X, dy = to.Y - from.Y; + return V_Radians2Dir(std::atan2(dy, dx)); +} + +static CoordStruct V_FLHAbsoluteOffset(CoordStruct flh, DirStruct facing) +{ + return AttachEffectClass::GetFLHAbsoluteCoords(CoordStruct::Empty, flh, facing); +} + +void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorState& s, ObjectClass* pInvoker, bool isBullet) +{ + if (!pObject) + return; + + if (s.DisabledTimer > 0) + { + s.DisabledTimer--; + return; + } + + bool skipFrame = pType->Vector_TimeStep > 1 && (s.CurrentFrame % pType->Vector_TimeStep != 0); + s.CurrentFrame++; + + if (skipFrame) + return; + + s.MovementFrames++; + s.NormalRotF += s.NormalStepF; + s.NormalRotL += s.NormalStepL; + s.NormalRotH += s.NormalStepH; + + auto GetPos = [&]() { return pObject->GetCoords(); }; + + if (!s.Initialized) + { + s.Initialized = true; + s.InitialOriginPos = GetPos(); + s.InitialLocation = GetPos(); + + if (isBullet) + { + auto const pB = static_cast(pObject); + double vx = pB->Velocity.X, vy = pB->Velocity.Y; + s.FacingRad = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + } + else + { + auto const pT = static_cast(pObject); + s.FacingRad = pT->PrimaryFacing.Current().GetRadian<32>(); + } + s.TiltRad = 0.0; + s.OriginFacing = s.FacingRad; + s.OriginTilt = 0.0; + + double speed = static_cast(pType->Vector_InitialSpeed >= 0 ? pType->Vector_InitialSpeed : 0); + if (pType->Vector_RandomSpeedMin != pType->Vector_RandomSpeedMax) + speed = V_Random(pType->Vector_RandomSpeedMin, pType->Vector_RandomSpeedMax); + s.CurrentSpeed = speed; + + s.ArcHeight = static_cast(pType->Vector_ArcHeight); + if (pType->Vector_ArcRandomHeightMin != pType->Vector_ArcRandomHeightMax) + s.ArcHeight = V_Random(pType->Vector_ArcRandomHeightMin, pType->Vector_ArcRandomHeightMax); + s.ArcRotation = pType->Vector_ArcRotation; + if (pType->Vector_ArcRandomRotationMin != pType->Vector_ArcRandomRotationMax) + s.ArcRotation = V_Random(pType->Vector_ArcRandomRotationMin, pType->Vector_ArcRandomRotationMax); + + if (pType->Vector_TargetFLH.isset()) + { + CoordStruct offset = pType->Vector_TargetFLH.Get(); + if (pType->Vector_TargetOffsetFMin != pType->Vector_TargetOffsetFMax) + offset.X = pType->Vector_TargetOffsetFMin + rand() % (pType->Vector_TargetOffsetFMax - pType->Vector_TargetOffsetFMin + 1); + if (pType->Vector_TargetOffsetLMin != pType->Vector_TargetOffsetLMax) + offset.Y = pType->Vector_TargetOffsetLMin + rand() % (pType->Vector_TargetOffsetLMax - pType->Vector_TargetOffsetLMin + 1); + if (pType->Vector_TargetOffsetHMin != pType->Vector_TargetOffsetHMax) + offset.Z = pType->Vector_TargetOffsetHMin + rand() % (pType->Vector_TargetOffsetHMax - pType->Vector_TargetOffsetHMin + 1); + s.TargetOffset = offset; + } + + if (pType->Vector_NormalVector.isset()) + { + s.NormalRotF = 0.0; s.NormalRotL = 0.0; s.NormalRotH = 0.0; + auto ns = [](double ps, double m1, double M1, double m2, double M2) { + if (ps != 0.0) return ps; + if (M2 > m2 && (rand() % 2)) return m2 + (rand() / (double)RAND_MAX) * (M2 - m2); + return M1 > m1 ? m1 + (rand() / (double)RAND_MAX) * (M1 - m1) : 0.0; + }; + s.NormalStepF = ns(pType->Vector_NormalFAnglePerStep, pType->Vector_NormalFAngleRMin, pType->Vector_NormalFAngleRMax, pType->Vector_NormalFAngleRMin2, pType->Vector_NormalFAngleRMax2); + s.NormalStepL = ns(pType->Vector_NormalLAnglePerStep, pType->Vector_NormalLAngleRMin, pType->Vector_NormalLAngleRMax, pType->Vector_NormalLAngleRMin2, pType->Vector_NormalLAngleRMax2); + s.NormalStepH = ns(pType->Vector_NormalHAnglePerStep, pType->Vector_NormalHAngleRMin, pType->Vector_NormalHAngleRMax, pType->Vector_NormalHAngleRMin2, pType->Vector_NormalHAngleRMax2); + } + + if (pType->Vector_OriginNormalVector.isset()) + { + auto ns = [](double ps, double m1, double M1, double m2, double M2) { + if (ps != 0.0) return ps; + if (M2 > m2 && (rand() % 2)) return m2 + (rand() / (double)RAND_MAX) * (M2 - m2); + return M1 > m1 ? m1 + (rand() / (double)RAND_MAX) * (M1 - m1) : 0.0; + }; + s.OriginNormalStepF = ns(pType->Vector_OriginNormalFAnglePerStep, pType->Vector_OriginNormalFAngleRMin, pType->Vector_OriginNormalFAngleRMax, pType->Vector_OriginNormalFAngleRMin2, pType->Vector_OriginNormalFAngleRMax2); + s.OriginNormalStepL = ns(pType->Vector_OriginNormalLAnglePerStep, pType->Vector_OriginNormalLAngleRMin, pType->Vector_OriginNormalLAngleRMax, pType->Vector_OriginNormalLAngleRMin2, pType->Vector_OriginNormalLAngleRMax2); + s.OriginNormalStepH = ns(pType->Vector_OriginNormalHAnglePerStep, pType->Vector_OriginNormalHAngleRMin, pType->Vector_OriginNormalHAngleRMax, pType->Vector_OriginNormalHAngleRMin2, pType->Vector_OriginNormalHAngleRMax2); + } + else + { + s.OriginNormalStepF = pType->Vector_OriginNormalFAnglePerStep; + s.OriginNormalStepL = pType->Vector_OriginNormalLAnglePerStep; + s.OriginNormalStepH = pType->Vector_OriginNormalHAnglePerStep; + } + } + + // === Dynamic facing === + double effectiveFacing = s.FacingRad + V_Deg2Rad(s.NormalRotH); + double effectiveTilt = s.TiltRad + V_Deg2Rad(s.NormalRotL); + DirStruct mainFacingDir = V_Radians2Dir(effectiveFacing); + + if (pType->Vector_OriginIsOnWorld) + { + mainFacingDir = DirStruct{}; + effectiveFacing = 0.0; + effectiveTilt = 0.0; + } + + bool hasNormal = pType->Vector_NormalVector.isset(); + if (!pType->Vector_OriginNoUpdate && !hasNormal && !pType->Vector_OriginIsOnWorld) + { + switch (pType->Vector_Origin) + { + case VectorOrigin::Source: + if (pInvoker) + { + mainFacingDir = V_Point2Dir(pInvoker->GetCoords(), GetPos()); + effectiveFacing = mainFacingDir.GetRadian<32>(); + if (pType->Vector_AllowedTilt) + { + double dx = GetPos().X - pInvoker->GetCoords().X; + double dy = GetPos().Y - pInvoker->GetCoords().Y; + double dz = GetPos().Z - pInvoker->GetCoords().Z; + double lenXY = std::sqrt(dx * dx + dy * dy); + effectiveTilt = (lenXY > 1e-6) ? std::atan2(dz, lenXY) : 0.0; + } + } + break; + case VectorOrigin::Target: + { + CoordStruct targetPos; + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) + targetPos = pB->Target->GetCoords(); + else + break; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) + targetPos = pT->Target->GetCoords(); + else + break; + } + mainFacingDir = V_Point2Dir(targetPos, GetPos()); + effectiveFacing = mainFacingDir.GetRadian<32>(); + if (pType->Vector_AllowedTilt) + { + double dx = GetPos().X - targetPos.X; + double dy = GetPos().Y - targetPos.Y; + double dz = GetPos().Z - targetPos.Z; + double lenXY = std::sqrt(dx * dx + dy * dy); + effectiveTilt = (lenXY > 1e-6) ? std::atan2(dz, lenXY) : 0.0; + } + break; + } + case VectorOrigin::Self: + if (isBullet) + { + double vx = static_cast(pObject)->Velocity.X; + double vy = static_cast(pObject)->Velocity.Y; + effectiveFacing = (vx != 0 || vy != 0) ? std::atan2(vy, vx) : 0.0; + mainFacingDir = V_Radians2Dir(effectiveFacing); + } + else + { + auto const pT = static_cast(pObject); + mainFacingDir = pType->Vector_OriginIsOnBody + ? pT->PrimaryFacing.Current() + : pT->TurretFacing(); + effectiveFacing = mainFacingDir.GetRadian<32>(); + } + break; + case VectorOrigin::Launcher: + if (pInvoker) + { + auto const pLT = static_cast(pInvoker); + mainFacingDir = pType->Vector_OriginIsOnBody + ? pLT->PrimaryFacing.Current() + : pLT->TurretFacing(); + effectiveFacing = mainFacingDir.GetRadian<32>(); + } + break; + } + } + + // === Origin coordinate (per-frame tracking) === + CoordStruct originPos = GetPos(); + + switch (pType->Vector_Origin) + { + case VectorOrigin::Target: + if (pType->Vector_OriginNoUpdate) + originPos = s.InitialOriginPos; + else if (isBullet) + { + auto const pB = static_cast(pObject); + originPos = pB->Target ? pB->Target->GetCoords() : pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + originPos = pT->Target ? pT->Target->GetCoords() : originPos; + } + break; + case VectorOrigin::Launcher: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Source: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : + (pInvoker ? pInvoker->GetCoords() : GetPos()); + break; + case VectorOrigin::Self: + originPos = pType->Vector_OriginNoUpdate ? s.InitialOriginPos : GetPos(); + break; + } + + if (pType->Vector_OriginFLH.isset() && pType->Vector_Origin != VectorOrigin::Self) + originPos = AttachEffectClass::GetFLHAbsoluteCoords(originPos, pType->Vector_OriginFLH, mainFacingDir); + + CoordStruct currentPos = GetPos(); + + // ================================================================ + // Circle mode + // ================================================================ + bool hasCircle = pType->Vector_CircleRadius > 0 || pType->Vector_CircleSpeed != 0 || pType->Vector_CircleAnglePerStep > 0.0 + || (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + || (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin); + + if (hasCircle) + { + double calcRadius = static_cast(pType->Vector_CircleRadius); + if (calcRadius <= 0.0) + { + double tdx = currentPos.X - originPos.X; + double tdy = currentPos.Y - originPos.Y; + calcRadius = std::sqrt(tdx * tdx + tdy * tdy); + } + + if (s.MovementFrames == 1) + { + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleSpeed); + s.CurrentCircleRadius = calcRadius; + if (pType->Vector_CircleRandomRadiusMax > pType->Vector_CircleRandomRadiusMin) + s.CurrentCircleRadius = static_cast(rand() % (pType->Vector_CircleRandomRadiusMax - pType->Vector_CircleRandomRadiusMin + 1) + pType->Vector_CircleRandomRadiusMin); + } + s.CurrentCircleSpeed += pType->Vector_CircleSpeedAcceleration; + if (pType->Vector_CircleMaxSpeed != 0 && s.CurrentCircleSpeed > pType->Vector_CircleMaxSpeed) + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleMaxSpeed); + if (pType->Vector_CircleMinSpeed != 0 && s.CurrentCircleSpeed < pType->Vector_CircleMinSpeed) + s.CurrentCircleSpeed = static_cast(pType->Vector_CircleMinSpeed); + + if (s.MovementFrames == 1) + { + s.CurrentCircleAngle = pType->Vector_CircleAnglePerStep; + if (pType->Vector_CircleRandomAngleMax > pType->Vector_CircleRandomAngleMin) + s.CurrentCircleAngle = V_Random(pType->Vector_CircleRandomAngleMin, pType->Vector_CircleRandomAngleMax); + } + s.CurrentCircleAngle += pType->Vector_CircleAngleAcceleration; + if (pType->Vector_CircleMaxAngle != 0 && s.CurrentCircleAngle > pType->Vector_CircleMaxAngle) + s.CurrentCircleAngle = pType->Vector_CircleMaxAngle; + if (pType->Vector_CircleMinAngle != 0 && s.CurrentCircleAngle < pType->Vector_CircleMinAngle) + s.CurrentCircleAngle = pType->Vector_CircleMinAngle; + + double speed = s.CurrentCircleSpeed; + double angleStep = s.CurrentCircleAngle; + if (speed <= 0.0 && angleStep > 0.0) + speed = calcRadius * V_Deg2Rad(angleStep); + else if (angleStep <= 0.0 && speed > 0.0) + angleStep = V_Rad2Deg(speed / calcRadius); + + CoordStruct circleCenter = originPos; + if (pType->Vector_CircleOrigin.isset()) + { + if (pType->Vector_AllowOriginTilt) + circleCenter = AttachEffectClass::GetFLHAbsoluteCoords(originPos, pType->Vector_CircleOrigin, mainFacingDir); + else + circleCenter = { originPos.X + pType->Vector_CircleOrigin.Get().X, originPos.Y + pType->Vector_CircleOrigin.Get().Y, originPos.Z + pType->Vector_CircleOrigin.Get().Z }; + } + + bool hasOriginSub = pType->Vector_OriginMoveTo.isset() + || pType->Vector_OriginTargetFLH.isset() + || pType->Vector_OriginCircleRadius >= 0 || pType->Vector_OriginCircleSpeed != 0 || pType->Vector_OriginCircleAnglePerStep != 0; + + if (hasOriginSub) + { + CoordStruct baseCenter = originPos; + + if (pType->Vector_OriginOrigin != VectorOrigin::Self) + { + switch (pType->Vector_OriginOrigin) + { + case VectorOrigin::Launcher: + if (pInvoker) baseCenter = pInvoker->GetCoords(); + break; + case VectorOrigin::Target: + if (isBullet) + { + auto const pB = static_cast(pObject); + if (pB->Target) baseCenter = pB->Target->GetCoords(); + else if (pB->Owner && pB->Owner->Target) baseCenter = pB->Owner->Target->GetCoords(); + else baseCenter = pB->TargetCoords; + } + else + { + auto const pT = static_cast(pObject); + if (pT->Target) baseCenter = pT->Target->GetCoords(); + else + { + auto const pFoot = abstract_cast(pT); + if (pFoot && pFoot->Destination) baseCenter = pFoot->Destination->GetCoords(); + } + } + break; + case VectorOrigin::Source: + if (pInvoker) baseCenter = pInvoker->GetCoords(); + break; + } + } + else if (pType->Vector_OriginOriginFLH.isset()) + { + baseCenter.X += pType->Vector_OriginOriginFLH.Get().X; + baseCenter.Y += pType->Vector_OriginOriginFLH.Get().Y; + baseCenter.Z += pType->Vector_OriginOriginFLH.Get().Z; + } + + if (s.MovementFrames == 1) + { + s.OriginOffset = { circleCenter.X - baseCenter.X, circleCenter.Y - baseCenter.Y, circleCenter.Z - baseCenter.Z }; + s.OriginCircleRadiusRuntime = static_cast(pType->Vector_OriginCircleRadius); + s.OriginCircleSpeedRuntime = static_cast(pType->Vector_OriginCircleSpeed); + s.OriginCircleAngleRuntime = 0.0; + s.OriginTargetOffset = CoordStruct::Empty; + + if (pType->Vector_OriginNormalVector.isset()) + { + CoordStruct nv = pType->Vector_OriginNormalVector.Get(); + double len = std::sqrt(static_cast(nv.X * nv.X + nv.Y * nv.Y)); + s.OriginFacing = len > 1e-6 ? std::atan2(static_cast(nv.Y), static_cast(nv.X)) : 0; + s.OriginTilt = len > 1e-6 ? std::atan2(static_cast(nv.Z), len) : (nv.Z > 0 ? 3.14159265358979323846 / 2.0 : -3.14159265358979323846 / 2.0); + } + } + + s.OriginNormalRotFRuntime += s.OriginNormalStepF; + s.OriginNormalRotLRuntime += s.OriginNormalStepL; + s.OriginNormalRotHRuntime += s.OriginNormalStepH; + + double oFacing = s.OriginFacing + V_Deg2Rad(s.OriginNormalRotHRuntime); + double oTilt = s.OriginTilt + V_Deg2Rad(s.OriginNormalRotLRuntime); + + CoordStruct originCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + CoordStruct disp{ 0, 0, 0 }; + + if (pType->Vector_OriginMoveTo.isset()) + { + s.OriginAngle += pType->Vector_OriginCircleAnglePerStep; + CoordStruct mto = pType->Vector_OriginMoveTo.Get(); + if (pType->Vector_OriginGrowRate.isset()) + { + mto.X += pType->Vector_OriginGrowRate.Get().X * s.OriginElapsed; + mto.Y += pType->Vector_OriginGrowRate.Get().Y * s.OriginElapsed; + mto.Z += pType->Vector_OriginGrowRate.Get().Z * s.OriginElapsed; + } + disp = V_FLHAbsoluteOffset(mto, V_Radians2Dir(oFacing + V_Deg2Rad(s.OriginAngle))); + } + else if (pType->Vector_OriginTargetFLH.isset()) + { + if (s.OriginElapsed == 0) + s.OriginSpeed = pType->Vector_OriginInitialSpeed >= 0 ? pType->Vector_OriginInitialSpeed : 40.0; + + CoordStruct targetWorld = AttachEffectClass::GetFLHAbsoluteCoords(baseCenter, pType->Vector_OriginTargetFLH.Get() + s.OriginTargetOffset, V_Radians2Dir(oFacing)); + int dx = targetWorld.X - originCenter.X, dy = targetWorld.Y - originCenter.Y, dz = targetWorld.Z - originCenter.Z; + double dist = std::sqrt(static_cast(dx * dx + dy * dy + dz * dz)); + if (dist >= 1.0) + { + double sv = s.OriginSpeed / dist; + disp = { static_cast(dx * sv), static_cast(dy * sv), static_cast(dz * sv) }; + } + } + else + { + s.OriginCircleRadiusRuntime += pType->Vector_OriginCircleRadiusGrow; + double tr = s.OriginCircleRadiusRuntime; + if (pType->Vector_OriginCircleMaxRadius > 0 && tr > pType->Vector_OriginCircleMaxRadius) tr = pType->Vector_OriginCircleMaxRadius; + if (pType->Vector_OriginCircleMinRadius > 0 && tr < pType->Vector_OriginCircleMinRadius) tr = pType->Vector_OriginCircleMinRadius; + double stepO = pType->Vector_OriginCircleAnglePerStep; + if (pType->Vector_OriginCircleSpeed != 0 && tr > 0) + stepO = V_Rad2Deg(pType->Vector_OriginCircleSpeed / tr); + s.OriginCircleAngleRuntime += stepO; + double r = V_Deg2Rad(stepO); + double ca = std::cos(r), sa = std::sin(r); + double dxO = static_cast(s.OriginOffset.X); + double dyO = static_cast(s.OriginOffset.Y); + double dzO = static_cast(s.OriginOffset.Z); + double cf = std::cos(oFacing), sf = std::sin(oFacing), ct = std::cos(oTilt), st = std::sin(oTilt); + double dL = dxO * (-sf) + dyO * cf; + double dH = dxO * (-cf * st) + dyO * (-sf * st) + dzO * ct; + double cd = std::sqrt(dL * dL + dH * dH); + if (cd < 1.0 && tr > 0) { dL = tr; dH = 0; cd = tr; } + else if (cd < 1.0) cd = 1.0; + double rL = (dL / cd * tr * ca) - (dH / cd * tr * sa); + double rH = (dL / cd * tr * sa) + (dH / cd * tr * ca); + CoordStruct newOffset; + newOffset.X = static_cast(rL * (-sf) + rH * (-cf * st)); + newOffset.Y = static_cast(rL * cf + rH * (-sf * st)); + newOffset.Z = static_cast(rH * ct); + disp.X = newOffset.X - s.OriginOffset.X; + disp.Y = newOffset.Y - s.OriginOffset.Y; + disp.Z = newOffset.Z - s.OriginOffset.Z; + } + + s.OriginOffset.X += disp.X; s.OriginOffset.Y += disp.Y; s.OriginOffset.Z += disp.Z; + circleCenter = { baseCenter.X + s.OriginOffset.X, baseCenter.Y + s.OriginOffset.Y, baseCenter.Z + s.OriginOffset.Z }; + s.OriginElapsed++; + } + + CoordStruct centerDelta{ 0, 0, 0 }; + bool useCenterTracking = false; + if (s.PrevCircleCenter.X || s.PrevCircleCenter.Y || s.PrevCircleCenter.Z) + { + centerDelta.X = circleCenter.X - s.PrevCircleCenter.X; + centerDelta.Y = circleCenter.Y - s.PrevCircleCenter.Y; + centerDelta.Z = circleCenter.Z - s.PrevCircleCenter.Z; + useCenterTracking = (pType->Vector_OriginCircleRadius >= 0 || pType->Vector_OriginCircleSpeed != 0 || pType->Vector_OriginCircleAnglePerStep != 0); + } + s.PrevCircleCenter = circleCenter; + + CoordStruct trackPos = currentPos; + if (useCenterTracking) + { + trackPos.X += centerDelta.X; trackPos.Y += centerDelta.Y; trackPos.Z += centerDelta.Z; + } + + double dx = static_cast(trackPos.X - circleCenter.X); + double dy = static_cast(trackPos.Y - circleCenter.Y); + double dz = static_cast(trackPos.Z - circleCenter.Z); + double currentDist; + bool useTiltPlane = hasNormal || (pType->Vector_AllowedTilt && effectiveTilt != 0.0); + + if (useTiltPlane) + { + double cosF = std::cos(effectiveFacing), sinF = std::sin(effectiveFacing); + double cosT = std::cos(effectiveTilt), sinT = std::sin(effectiveTilt); + double dL = dx * (-sinF) + dy * cosF; + double dH = dx * (-cosF * sinT) + dy * (-sinF * sinT) + dz * cosT; + currentDist = std::sqrt(dL * dL + dH * dH); + } + else + { + currentDist = std::sqrt(dx * dx + dy * dy); + } + bool startAtCenter = currentDist < 1.0; + if (currentDist < 1.0) currentDist = 1.0; + + s.CurrentCircleRadius += pType->Vector_CircleRadiusGrow; + double targetRadius = s.CurrentCircleRadius; + if (pType->Vector_CircleMaxRadius > 0 && targetRadius > pType->Vector_CircleMaxRadius) + targetRadius = static_cast(pType->Vector_CircleMaxRadius); + if (pType->Vector_CircleMinRadius > 0 && targetRadius < pType->Vector_CircleMinRadius) + targetRadius = static_cast(pType->Vector_CircleMinRadius); + + if (startAtCenter) + { + dx = targetRadius; dy = 0.0; currentDist = targetRadius; + } + + double rad = V_Deg2Rad(angleStep); + double cosA = std::cos(rad), sinA = std::sin(rad); + CoordStruct moveDisp{ 0, 0, 0 }; + + if (useTiltPlane) + { + double cosF = std::cos(effectiveFacing), sinF = std::sin(effectiveFacing); + double cosT = std::cos(effectiveTilt), sinT = std::sin(effectiveTilt); + double dL = dx * (-sinF) + dy * cosF; + double dH = dx * (-cosF * sinT) + dy * (-sinF * sinT) + dz * cosT; + double curDist = std::sqrt(dL * dL + dH * dH); + if (curDist < 1.0) curDist = 1.0; + double ndL = dL / curDist * targetRadius; + double ndH = dH / curDist * targetRadius; + double rL = ndL * cosA - ndH * sinA; + double rH = ndL * sinA + ndH * cosA; + moveDisp.X = circleCenter.X + static_cast(rL * (-sinF) + rH * (-cosF * sinT)) - currentPos.X; + moveDisp.Y = circleCenter.Y + static_cast(rL * cosF + rH * (-sinF * sinT)) - currentPos.Y; + moveDisp.Z = circleCenter.Z + static_cast(rH * cosT) - currentPos.Z; + } + else + { + double ndx = dx / currentDist * targetRadius; + double ndy = dy / currentDist * targetRadius; + double rx = ndx * cosA - ndy * sinA; + double ry = ndx * sinA + ndy * cosA; + moveDisp.X = circleCenter.X + static_cast(rx) - currentPos.X; + moveDisp.Y = circleCenter.Y + static_cast(ry) - currentPos.Y; + moveDisp.Z = circleCenter.Z - currentPos.Z; + } + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = moveDisp.X; + pB->Velocity.Y = moveDisp.Y; + pB->Velocity.Z = moveDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + + if (pType->Vector_CircleEndOnMaxRadius && pType->Vector_CircleMaxRadius > 0 && s.CurrentCircleRadius >= pType->Vector_CircleMaxRadius) + return; + if (pType->Vector_CircleEndOnMinRadius && pType->Vector_CircleMinRadius > 0 && s.CurrentCircleRadius <= pType->Vector_CircleMinRadius) + return; + + return; +} + +// ================================================================ +// MoveTo mode +// ================================================================ + if (static_cast(pType->Vector_MoveTo) != CoordStruct::Empty) + { + DirStruct moveDir = mainFacingDir; + if (pType->Vector_AnglePerStep != 0.0) + { + if (s.MovementFrames == 1) + s.CurrentAngle = 0.0; + s.CurrentAngle += pType->Vector_AnglePerStep; + moveDir = V_Radians2Dir(mainFacingDir.GetRadian<32>() + V_Deg2Rad(s.CurrentAngle)); + } + + CoordStruct grow{ static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().X * s.MovementFrames : 0), + static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().Y * s.MovementFrames : 0), + static_cast(pType->Vector_GrowRate.isset() ? pType->Vector_GrowRate.Get().Z * s.MovementFrames : 0) }; + CoordStruct mt = pType->Vector_MoveTo; + CoordStruct moveFlh = { mt.X + grow.X, mt.Y + grow.Y, mt.Z + grow.Z }; + + CoordStruct moveDisp = V_FLHAbsoluteOffset(moveFlh, moveDir); + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = moveDisp.X; pB->Velocity.Y = moveDisp.Y; pB->Velocity.Z = moveDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + moveDisp.X, currentPos.Y + moveDisp.Y, currentPos.Z + moveDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + return; + } + + // ================================================================ + // TargetFLH modes: ReachTarget / Speed + // ================================================================ + if (!pType->Vector_TargetFLH.isset()) + { + if (!isBullet && pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + return; + } + + CoordStruct frameTargetFlh = { pType->Vector_TargetFLH.Get().X + s.TargetOffset.X, + pType->Vector_TargetFLH.Get().Y + s.TargetOffset.Y, + pType->Vector_TargetFLH.Get().Z + s.TargetOffset.Z }; + CoordStruct frameTarget; + + switch (pType->Vector_Origin) + { + case VectorOrigin::Launcher: + if (pInvoker) + frameTarget = TechnoExt::GetFLHAbsoluteCoords(static_cast(pInvoker), frameTargetFlh, !pType->Vector_OriginIsOnBody); + else + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, mainFacingDir); + break; + case VectorOrigin::Self: + if (pType->Vector_OriginIsOnWorld) + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, DirStruct{}); + else if (isBullet) + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(currentPos, frameTargetFlh, mainFacingDir); + else + frameTarget = TechnoExt::GetFLHAbsoluteCoords(static_cast(pObject), frameTargetFlh, !pType->Vector_OriginIsOnBody); + break; + default: + frameTarget = AttachEffectClass::GetFLHAbsoluteCoords(originPos, frameTargetFlh, mainFacingDir); + break; + } + + CoordStruct dirVec = { frameTarget.X - currentPos.X, frameTarget.Y - currentPos.Y, frameTarget.Z - currentPos.Z }; + double dirLen = std::sqrt(static_cast(dirVec.X * dirVec.X + dirVec.Y * dirVec.Y + dirVec.Z * dirVec.Z)); + CoordStruct resultDisp{ 0, 0, 0 }; + + if (pType->Vector_ReachTarget) + { + int totalDuration = pType->Duration / pType->Vector_TimeStep; + if (totalDuration < 1) totalDuration = 1; + int effectiveDuration = totalDuration - pType->Vector_DisabledFrames; + if (effectiveDuration < 1) effectiveDuration = 1; + int remainingFrames = effectiveDuration - s.MovementFrames + 1; + + if (pType->Vector_ReachTargetEarlyEnd > 0 && pType->Vector_ReachTargetEarlyEnd < effectiveDuration + && remainingFrames <= pType->Vector_ReachTargetEarlyEnd) + { + s.DisabledTimer = pType->Vector_ReachTargetEarlyEnd; + return; + } + + if (remainingFrames <= 0) + { + if (pType->Vector_Force) + { + resultDisp.X = frameTarget.X - currentPos.X; + resultDisp.Y = frameTarget.Y - currentPos.Y; + resultDisp.Z = frameTarget.Z - currentPos.Z; + } + } + else if (dirLen > 1e-6) + { + double adjustedSpeed = dirLen / remainingFrames; + resultDisp.X = static_cast(dirVec.X / dirLen * adjustedSpeed); + resultDisp.Y = static_cast(dirVec.Y / dirLen * adjustedSpeed); + resultDisp.Z = static_cast(dirVec.Z / dirLen * adjustedSpeed); + + if (s.ArcHeight != 0) + { + double t = static_cast(s.MovementFrames - 1) / effectiveDuration; + double tNext = static_cast(s.MovementFrames) / effectiveDuration; + double peakScale = pType->Vector_ArcPeakPercent > 0 ? 1.0 / (pType->Vector_ArcPeakPercent * (1.0 - pType->Vector_ArcPeakPercent)) : 4.0; + double delta = peakScale * s.ArcHeight * (tNext * (1.0 - tNext) - t * (1.0 - t)); + + if (s.ArcRotation == 0.0) + { + resultDisp.Z += static_cast(delta); + } + else + { + double ax = frameTarget.X - s.InitialLocation.X; + double ay = frameTarget.Y - s.InitialLocation.Y; + double az = frameTarget.Z - s.InitialLocation.Z; + double aLen = std::sqrt(ax * ax + ay * ay + az * az); + if (aLen > 1e-6) + { + double dnx = ax / aLen, dny = ay / aLen, dnz = az / aLen; + double upDotD = dnz; + double px = -dnx * upDotD, py = -dny * upDotD, pz = 1.0 - dnz * upDotD; + double pLen = std::sqrt(px * px + py * py + pz * pz); + if (pLen < 1e-6) { px = 1.0 - dnx * dnx; py = -dny * dnx; pz = -dnz * dnx; pLen = std::sqrt(px * px + py * py + pz * pz); } + double pnx = px / pLen, pny = py / pLen, pnz = pz / pLen; + double arcRad = V_Deg2Rad(s.ArcRotation); + double c = std::cos(arcRad), s_arc = std::sin(arcRad); + double rx = pnx * c + (dny * pnz - dnz * pny) * s_arc; + double ry = pny * c + (dnz * pnx - dnx * pnz) * s_arc; + double rz = pnz * c + (dnx * pny - dny * pnx) * s_arc; + resultDisp.X += static_cast(rx * delta); + resultDisp.Y += static_cast(ry * delta); + resultDisp.Z += static_cast(rz * delta); + } + else + { + resultDisp.Z += static_cast(delta); + } + } + } + } + } + else if (dirLen > 1e-6) + { + double spd = s.CurrentSpeed; + if (pType->Vector_Acceleration != 0) spd += pType->Vector_Acceleration * s.CurrentFrame; + if (pType->Vector_MinSpeed >= 0 && spd < pType->Vector_MinSpeed) spd = pType->Vector_MinSpeed; + if (pType->Vector_MaxSpeed >= 0 && spd > pType->Vector_MaxSpeed) spd = pType->Vector_MaxSpeed; + resultDisp.X = static_cast(dirVec.X / dirLen * spd); + resultDisp.Y = static_cast(dirVec.Y / dirLen * spd); + resultDisp.Z = static_cast(dirVec.Z / dirLen * spd); + } + + if (isBullet) + { + auto const pB = static_cast(pObject); + pB->Velocity.X = resultDisp.X; + pB->Velocity.Y = resultDisp.Y; + pB->Velocity.Z = resultDisp.Z; + } + else + { + pObject->SetLocation({ currentPos.X + resultDisp.X, currentPos.Y + resultDisp.Y, currentPos.Z + resultDisp.Z }); + if (pType->Vector_Freeze) + { + if (auto const pFoot = abstract_cast(pObject)) + pFoot->StopMoving(); + } + } + + if (!isBullet && pType->Vector_SyncFacing && (resultDisp.X != 0 || resultDisp.Y != 0)) + { + double ang = std::atan2(static_cast(resultDisp.Y), static_cast(resultDisp.X)); + auto dir = DirStruct(static_cast(ang * 32768.0 / 3.14159265358979323846)); + static_cast(pObject)->PrimaryFacing.SetDesired(dir); + } + + if (pType->Vector_AllowFallingDestroy && pType->Vector_FallingDestroyHeight > 0) + { + if (s.InitialLocation.Z - GetPos().Z > pType->Vector_FallingDestroyHeight) + return; + } +} diff --git a/src/New/Entity/VectorState.h b/src/New/Entity/VectorState.h new file mode 100644 index 0000000000..1d6b0f3e2a --- /dev/null +++ b/src/New/Entity/VectorState.h @@ -0,0 +1,98 @@ +#pragma once + +#include + +#include + +struct VectorState +{ + bool Initialized; + int CurrentFrame; + int DisabledTimer; + double CurrentSpeed; + double CurrentAngle; + double CurrentCircleRadius; + double CurrentCircleSpeed; + double CurrentCircleAngle; + CoordStruct InitialOriginPos; + CoordStruct InitialLocation; + CoordStruct PrevCirclePos; + double ArcHeight; + double ArcRotation; + double ArcPeakPercent; + CoordStruct TargetOffset; + double NormalRotF; + double NormalRotL; + double NormalRotH; + double NormalStepF; + double NormalStepL; + double NormalStepH; + int MovementFrames; + double FacingRad; + double TiltRad; + CoordStruct OriginOffset; + CoordStruct PrevCircleCenter; + int OriginElapsed; + double OriginSpeed; + double OriginAngle; + CoordStruct OriginTargetOffset; + double OriginCircleRadiusRuntime; + double OriginCircleSpeedRuntime; + double OriginCircleAngleRuntime; + double OriginNormalRotFRuntime; + double OriginNormalRotLRuntime; + double OriginNormalRotHRuntime; + double OriginNormalStepF; + double OriginNormalStepL; + double OriginNormalStepH; + double OriginFacing; + double OriginTilt; + CoordStruct StoredDisp; + + VectorState() + : Initialized { false } + , CurrentFrame { 0 } + , DisabledTimer { 0 } + , CurrentSpeed { 0.0 } + , CurrentAngle { 0.0 } + , CurrentCircleRadius { 0.0 } + , CurrentCircleSpeed { 0.0 } + , CurrentCircleAngle { 0.0 } + , InitialOriginPos { CoordStruct::Empty } + , InitialLocation { CoordStruct::Empty } + , PrevCirclePos { CoordStruct::Empty } + , ArcHeight { 0.0 } + , ArcRotation { 0.0 } + , ArcPeakPercent { 0.5 } + , TargetOffset { CoordStruct::Empty } + , NormalRotF { 0.0 } + , NormalRotL { 0.0 } + , NormalRotH { 0.0 } + , NormalStepF { 0.0 } + , NormalStepL { 0.0 } + , NormalStepH { 0.0 } + , MovementFrames { 0 } + , FacingRad { 0.0 } + , TiltRad { 0.0 } + , OriginOffset { CoordStruct::Empty } + , PrevCircleCenter { CoordStruct::Empty } + , OriginElapsed { 0 } + , OriginSpeed { 0.0 } + , OriginAngle { 0.0 } + , OriginTargetOffset { CoordStruct::Empty } + , OriginCircleRadiusRuntime { 0.0 } + , OriginCircleSpeedRuntime { 0.0 } + , OriginCircleAngleRuntime { 0.0 } + , OriginNormalRotFRuntime { 0.0 } + , OriginNormalRotLRuntime { 0.0 } + , OriginNormalRotHRuntime { 0.0 } + , OriginNormalStepF { 0.0 } + , OriginNormalStepL { 0.0 } + , OriginNormalStepH { 0.0 } + , OriginFacing { 0.0 } + , OriginTilt { 0.0 } + , StoredDisp { CoordStruct::Empty } + { } +}; + +void VectorAI_Run(ObjectClass* pObject, AttachEffectTypeClass* pType, VectorState& state, ObjectClass* pInvoker, bool isBullet); diff --git a/src/New/Type/AttachEffectTypeClass.cpp b/src/New/Type/AttachEffectTypeClass.cpp index 941cfea4ad..669d17af9b 100644 --- a/src/New/Type/AttachEffectTypeClass.cpp +++ b/src/New/Type/AttachEffectTypeClass.cpp @@ -126,6 +126,7 @@ void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) this->ExpireWeapon_TriggerOn.Read(exINI, pSection, "ExpireWeapon.TriggerOn"); this->ExpireWeapon_CumulativeOnlyOnce.Read(exINI, pSection, "ExpireWeapon.CumulativeOnlyOnce"); this->ExpireWeapon_UseInvokerAsOwner.Read(exINI, pSection, "ExpireWeapon.UseInvokerAsOwner"); + this->Next.Read(exINI, pSection, "Next"); this->Tint_Color.Read(exINI, pSection, "Tint.Color"); this->Tint_Intensity.Read(exINI, pSection, "Tint.Intensity"); @@ -179,6 +180,144 @@ void AttachEffectTypeClass::LoadFromINI(CCINIClass* pINI) this->Unkillable.Read(exINI, pSection, "Unkillable"); this->LaserTrail_Type.Read(exINI, pSection, "LaserTrail.Type"); + // Vector + this->Vector_TimeStep.Read(exINI, pSection, "Vector.TimeStep"); + if (this->Vector_TimeStep < 1) + this->Vector_TimeStep = 1; + this->Vector_DisabledFrames.Read(exINI, pSection, "Vector.DisabledFrames"); + this->Vector_SyncFacing.Read(exINI, pSection, "Vector.SyncFacing"); + this->Vector_OriginIsOnWorld.Read(exINI, pSection, "Vector.OriginIsOnWorld"); + this->Vector_OriginIsOnBody.Read(exINI, pSection, "Vector.OriginIsOnBody"); + this->Vector_Origin.Read(exINI, pSection, "Vector.Origin"); + this->Vector_OriginFLH.Read(exINI, pSection, "Vector.OriginFLH"); + this->Vector_OriginNoUpdate.Read(exINI, pSection, "Vector.OriginNoUpdate"); + this->Vector_Force.Read(exINI, pSection, "Vector.Force"); + this->Vector_Freeze.Read(exINI, pSection, "Vector.Freeze"); + this->Vector_AllowedTilt.Read(exINI, pSection, "Vector.AllowedTilt"); + this->Vector_NormalVector.Read(exINI, pSection, "Vector.NormalVector"); + this->Vector_NormalRandomF.Read(exINI, pSection, "Vector.NormalRandomF"); + this->Vector_NormalRandomL.Read(exINI, pSection, "Vector.NormalRandomL"); + this->Vector_NormalRandomH.Read(exINI, pSection, "Vector.NormalRandomH"); + this->Vector_NormalRandomFMin.Read(exINI, pSection, "Vector.NormalRandomF.Min"); + this->Vector_NormalRandomFMax.Read(exINI, pSection, "Vector.NormalRandomF.Max"); + this->Vector_NormalRandomLMin.Read(exINI, pSection, "Vector.NormalRandomL.Min"); + this->Vector_NormalRandomLMax.Read(exINI, pSection, "Vector.NormalRandomL.Max"); + this->Vector_NormalRandomHMin.Read(exINI, pSection, "Vector.NormalRandomH.Min"); + this->Vector_NormalRandomHMax.Read(exINI, pSection, "Vector.NormalRandomH.Max"); + this->Vector_NormalFAnglePerStep.Read(exINI, pSection, "Vector.NormalFAnglePerStep"); + this->Vector_NormalLAnglePerStep.Read(exINI, pSection, "Vector.NormalLAnglePerStep"); + this->Vector_NormalHAnglePerStep.Read(exINI, pSection, "Vector.NormalHAnglePerStep"); + this->Vector_MoveTo.Read(exINI, pSection, "Vector.MoveTo"); + this->Vector_GrowRate.Read(exINI, pSection, "Vector.GrowRate"); + this->Vector_AnglePerStep.Read(exINI, pSection, "Vector.AnglePerStep"); + this->Vector_CircleRadius.Read(exINI, pSection, "Vector.CircleRadius"); + this->Vector_CircleSpeed.Read(exINI, pSection, "Vector.CircleSpeed"); + this->Vector_CircleSpeedAcceleration.Read(exINI, pSection, "Vector.CircleSpeedAcceleration"); + this->Vector_CircleMaxSpeed.Read(exINI, pSection, "Vector.CircleMaxSpeed"); + this->Vector_CircleMinSpeed.Read(exINI, pSection, "Vector.CircleMinSpeed"); + this->Vector_CircleAnglePerStep.Read(exINI, pSection, "Vector.CircleAnglePerStep"); + this->Vector_CircleAngleAcceleration.Read(exINI, pSection, "Vector.CircleAngleAcceleration"); + this->Vector_CircleOrigin.Read(exINI, pSection, "Vector.CircleOrigin"); + this->Vector_AllowOriginTilt.Read(exINI, pSection, "Vector.AllowOriginTilt"); + this->Vector_CircleRadiusGrow.Read(exINI, pSection, "Vector.CircleRadiusGrow"); + this->Vector_CircleMaxRadius.Read(exINI, pSection, "Vector.CircleMaxRadius"); + this->Vector_CircleMinRadius.Read(exINI, pSection, "Vector.CircleMinRadius"); + this->Vector_CircleEndOnMaxRadius.Read(exINI, pSection, "Vector.CircleEndOnMaxRadius"); + this->Vector_CircleEndOnMinRadius.Read(exINI, pSection, "Vector.CircleEndOnMinRadius"); + this->Vector_TargetFLH.Read(exINI, pSection, "Vector.TargetFLH"); + this->Vector_ReachTarget.Read(exINI, pSection, "Vector.ReachTarget"); + this->Vector_ReachTargetEarlyEnd.Read(exINI, pSection, "Vector.ReachTargetEarlyEnd"); + this->Vector_ArcHeight.Read(exINI, pSection, "Vector.ArcHeight"); + this->Vector_ArcPeakPercent.Read(exINI, pSection, "Vector.ArcPeakPercent"); + this->Vector_ArcPeakRandomPercent.Read(exINI, pSection, "Vector.ArcPeakRandomPercent"); + this->Vector_ArcPeakRandomPercentMin.Read(exINI, pSection, "Vector.ArcPeakRandomPercent.Min"); + this->Vector_ArcPeakRandomPercentMax.Read(exINI, pSection, "Vector.ArcPeakRandomPercent.Max"); + this->Vector_ArcRotation.Read(exINI, pSection, "Vector.ArcRotation"); + this->Vector_InitialSpeed.Read(exINI, pSection, "Vector.InitialSpeed"); + this->Vector_MaxSpeed.Read(exINI, pSection, "Vector.MaxSpeed"); + this->Vector_MinSpeed.Read(exINI, pSection, "Vector.MinSpeed"); + this->Vector_Acceleration.Read(exINI, pSection, "Vector.Acceleration"); + this->Vector_AllowFallingDestroy.Read(exINI, pSection, "Vector.AllowFallingDestroy"); + this->Vector_FallingDestroyHeight.Read(exINI, pSection, "Vector.FallingDestroyHeight"); + this->Vector_AffectTechno.Read(exINI, pSection, "Vector.AffectTechno"); + this->Vector_AffectBullets.Read(exINI, pSection, "Vector.AffectBullets"); + this->Vector_CircleRandomRadiusMin.Read(exINI, pSection, "Vector.CircleRandomRadius.Min"); + this->Vector_CircleRandomRadiusMax.Read(exINI, pSection, "Vector.CircleRandomRadius.Max"); + this->Vector_CircleRandomAngleMin.Read(exINI, pSection, "Vector.CircleRandomAngle.Min"); + this->Vector_CircleRandomAngleMax.Read(exINI, pSection, "Vector.CircleRandomAngle.Max"); + this->Vector_CircleRandomAngleMin2.Read(exINI, pSection, "Vector.CircleRandomAngle.Min2"); + this->Vector_CircleRandomAngleMax2.Read(exINI, pSection, "Vector.CircleRandomAngle.Max2"); + this->Vector_CircleMaxAngle.Read(exINI, pSection, "Vector.CircleMaxAngle"); + this->Vector_CircleMinAngle.Read(exINI, pSection, "Vector.CircleMinAngle"); + this->Vector_TargetOffsetFMin.Read(exINI, pSection, "Vector.TargetOffsetF.Min"); + this->Vector_TargetOffsetFMax.Read(exINI, pSection, "Vector.TargetOffsetF.Max"); + this->Vector_TargetOffsetLMin.Read(exINI, pSection, "Vector.TargetOffsetL.Min"); + this->Vector_TargetOffsetLMax.Read(exINI, pSection, "Vector.TargetOffsetL.Max"); + this->Vector_TargetOffsetHMin.Read(exINI, pSection, "Vector.TargetOffsetH.Min"); + this->Vector_TargetOffsetHMax.Read(exINI, pSection, "Vector.TargetOffsetH.Max"); + this->Vector_ArcRandomHeightMin.Read(exINI, pSection, "Vector.ArcRandomHeight.Min"); + this->Vector_ArcRandomHeightMax.Read(exINI, pSection, "Vector.ArcRandomHeight.Max"); + this->Vector_ArcRandomRotationMin.Read(exINI, pSection, "Vector.ArcRandomRotation.Min"); + this->Vector_ArcRandomRotationMax.Read(exINI, pSection, "Vector.ArcRandomRotation.Max"); + this->Vector_RandomSpeedMin.Read(exINI, pSection, "Vector.RandomSpeed.Min"); + this->Vector_RandomSpeedMax.Read(exINI, pSection, "Vector.RandomSpeed.Max"); + this->Vector_OriginMoveTo.Read(exINI, pSection, "Vector.Origin.MoveTo"); + this->Vector_OriginGrowRate.Read(exINI, pSection, "Vector.Origin.GrowRate"); + this->Vector_OriginTargetFLH.Read(exINI, pSection, "Vector.Origin.TargetFLH"); + this->Vector_OriginInitialSpeed.Read(exINI, pSection, "Vector.Origin.InitialSpeed"); + this->Vector_OriginAcceleration.Read(exINI, pSection, "Vector.Origin.Acceleration"); + this->Vector_OriginMaxSpeed.Read(exINI, pSection, "Vector.Origin.MaxSpeed"); + this->Vector_OriginMinSpeed.Read(exINI, pSection, "Vector.Origin.MinSpeed"); + this->Vector_OriginTargetOffsetFMin.Read(exINI, pSection, "Vector.Origin.TargetOffsetF.Min"); + this->Vector_OriginTargetOffsetFMax.Read(exINI, pSection, "Vector.Origin.TargetOffsetF.Max"); + this->Vector_OriginTargetOffsetLMin.Read(exINI, pSection, "Vector.Origin.TargetOffsetL.Min"); + this->Vector_OriginTargetOffsetLMax.Read(exINI, pSection, "Vector.Origin.TargetOffsetL.Max"); + this->Vector_OriginTargetOffsetHMin.Read(exINI, pSection, "Vector.Origin.TargetOffsetH.Min"); + this->Vector_OriginTargetOffsetHMax.Read(exINI, pSection, "Vector.Origin.TargetOffsetH.Max"); + this->Vector_OriginReachTarget.Read(exINI, pSection, "Vector.Origin.ReachTarget"); + this->Vector_OriginArcHeight.Read(exINI, pSection, "Vector.Origin.ArcHeight"); + this->Vector_OriginCircleRadius.Read(exINI, pSection, "Vector.Origin.CircleRadius"); + this->Vector_OriginCircleSpeed.Read(exINI, pSection, "Vector.Origin.CircleSpeed"); + this->Vector_OriginCircleAnglePerStep.Read(exINI, pSection, "Vector.Origin.CircleAnglePerStep"); + this->Vector_OriginLissajous.Read(exINI, pSection, "Vector.Origin.Lissajous"); + this->Vector_OriginCircleRadiusGrow.Read(exINI, pSection, "Vector.Origin.CircleRadiusGrow"); + this->Vector_OriginCircleMaxRadius.Read(exINI, pSection, "Vector.Origin.CircleMaxRadius"); + this->Vector_OriginCircleMinRadius.Read(exINI, pSection, "Vector.Origin.CircleMinRadius"); + this->Vector_OriginCircleEndOnMaxRadius.Read(exINI, pSection, "Vector.Origin.CircleEndOnMaxRadius"); + this->Vector_OriginCircleEndOnMinRadius.Read(exINI, pSection, "Vector.Origin.CircleEndOnMinRadius"); + this->Vector_OriginNormalVector.Read(exINI, pSection, "Vector.Origin.NormalVector"); + this->Vector_OriginNormalFAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalFAnglePerStep"); + this->Vector_OriginNormalLAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalLAnglePerStep"); + this->Vector_OriginNormalHAnglePerStep.Read(exINI, pSection, "Vector.Origin.NormalHAnglePerStep"); + this->Vector_OriginAllowedTilt.Read(exINI, pSection, "Vector.Origin.AllowedTilt"); + this->Vector_OriginCircleOffset.Read(exINI, pSection, "Vector.Origin.CircleOffset"); + this->Vector_OriginOrigin.Read(exINI, pSection, "Vector.Origin.Origin"); + this->Vector_OriginOriginFLH.Read(exINI, pSection, "Vector.Origin.OriginFLH"); + this->Vector_NormalFAngleRMin.Read(exINI, pSection, "Vector.NormalFAngleRanges.Min"); + this->Vector_NormalFAngleRMax.Read(exINI, pSection, "Vector.NormalFAngleRanges.Max"); + this->Vector_NormalFAngleRMin2.Read(exINI, pSection, "Vector.NormalFAngleRanges.Min2"); + this->Vector_NormalFAngleRMax2.Read(exINI, pSection, "Vector.NormalFAngleRanges.Max2"); + this->Vector_NormalLAngleRMin.Read(exINI, pSection, "Vector.NormalLAngleRanges.Min"); + this->Vector_NormalLAngleRMax.Read(exINI, pSection, "Vector.NormalLAngleRanges.Max"); + this->Vector_NormalLAngleRMin2.Read(exINI, pSection, "Vector.NormalLAngleRanges.Min2"); + this->Vector_NormalLAngleRMax2.Read(exINI, pSection, "Vector.NormalLAngleRanges.Max2"); + this->Vector_NormalHAngleRMin.Read(exINI, pSection, "Vector.NormalHAngleRanges.Min"); + this->Vector_NormalHAngleRMax.Read(exINI, pSection, "Vector.NormalHAngleRanges.Max"); + this->Vector_NormalHAngleRMin2.Read(exINI, pSection, "Vector.NormalHAngleRanges.Min2"); + this->Vector_NormalHAngleRMax2.Read(exINI, pSection, "Vector.NormalHAngleRanges.Max2"); + this->Vector_OriginNormalFAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Min"); + this->Vector_OriginNormalFAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Max"); + this->Vector_OriginNormalFAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Min2"); + this->Vector_OriginNormalFAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalFAngleRanges.Max2"); + this->Vector_OriginNormalLAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Min"); + this->Vector_OriginNormalLAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Max"); + this->Vector_OriginNormalLAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Min2"); + this->Vector_OriginNormalLAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalLAngleRanges.Max2"); + this->Vector_OriginNormalHAngleRMin.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Min"); + this->Vector_OriginNormalHAngleRMax.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Max"); + this->Vector_OriginNormalHAngleRMin2.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Min2"); + this->Vector_OriginNormalHAngleRMax2.Read(exINI, pSection, "Vector.Origin.NormalHAngleRanges.Max2"); + // Groups exINI.ParseStringList(this->Groups, pSection, "Groups"); AddToGroupsMap(); @@ -214,6 +353,7 @@ void AttachEffectTypeClass::Serialize(T& Stm) .Process(this->ExpireWeapon_TriggerOn) .Process(this->ExpireWeapon_CumulativeOnlyOnce) .Process(this->ExpireWeapon_UseInvokerAsOwner) + .Process(this->Next) .Process(this->Tint_Color) .Process(this->Tint_Intensity) .Process(this->Tint_VisibleToHouses) @@ -248,6 +388,140 @@ void AttachEffectTypeClass::Serialize(T& Stm) .Process(this->DisableWeapons) .Process(this->Unkillable) .Process(this->LaserTrail_Type) + .Process(this->Vector_TimeStep) + .Process(this->Vector_DisabledFrames) + .Process(this->Vector_SyncFacing) + .Process(this->Vector_OriginIsOnWorld) + .Process(this->Vector_OriginIsOnBody) + .Process(this->Vector_Origin) + .Process(this->Vector_OriginFLH) + .Process(this->Vector_OriginNoUpdate) + .Process(this->Vector_Force) + .Process(this->Vector_Freeze) + .Process(this->Vector_AllowedTilt) + .Process(this->Vector_NormalVector) + .Process(this->Vector_NormalRandomF) + .Process(this->Vector_NormalRandomL) + .Process(this->Vector_NormalRandomH) + .Process(this->Vector_NormalRandomFMin) + .Process(this->Vector_NormalRandomFMax) + .Process(this->Vector_NormalRandomLMin) + .Process(this->Vector_NormalRandomLMax) + .Process(this->Vector_NormalRandomHMin) + .Process(this->Vector_NormalRandomHMax) + .Process(this->Vector_NormalFAnglePerStep) + .Process(this->Vector_NormalLAnglePerStep) + .Process(this->Vector_NormalHAnglePerStep) + .Process(this->Vector_MoveTo) + .Process(this->Vector_GrowRate) + .Process(this->Vector_AnglePerStep) + .Process(this->Vector_CircleRadius) + .Process(this->Vector_CircleSpeed) + .Process(this->Vector_CircleSpeedAcceleration) + .Process(this->Vector_CircleMaxSpeed) + .Process(this->Vector_CircleMinSpeed) + .Process(this->Vector_CircleAnglePerStep) + .Process(this->Vector_CircleAngleAcceleration) + .Process(this->Vector_CircleOrigin) + .Process(this->Vector_AllowOriginTilt) + .Process(this->Vector_CircleRadiusGrow) + .Process(this->Vector_CircleMaxRadius) + .Process(this->Vector_CircleMinRadius) + .Process(this->Vector_CircleEndOnMaxRadius) + .Process(this->Vector_CircleEndOnMinRadius) + .Process(this->Vector_TargetFLH) + .Process(this->Vector_ReachTarget) + .Process(this->Vector_ReachTargetEarlyEnd) + .Process(this->Vector_ArcHeight) + .Process(this->Vector_ArcPeakPercent) + .Process(this->Vector_ArcPeakRandomPercent) + .Process(this->Vector_ArcPeakRandomPercentMin) + .Process(this->Vector_ArcPeakRandomPercentMax) + .Process(this->Vector_ArcRotation) + .Process(this->Vector_InitialSpeed) + .Process(this->Vector_MaxSpeed) + .Process(this->Vector_MinSpeed) + .Process(this->Vector_Acceleration) + .Process(this->Vector_AllowFallingDestroy) + .Process(this->Vector_FallingDestroyHeight) + .Process(this->Vector_AffectTechno) + .Process(this->Vector_AffectBullets) + .Process(this->Vector_CircleRandomRadiusMin) + .Process(this->Vector_CircleRandomRadiusMax) + .Process(this->Vector_CircleRandomAngleMin) + .Process(this->Vector_CircleRandomAngleMax) + .Process(this->Vector_CircleRandomAngleMin2) + .Process(this->Vector_CircleRandomAngleMax2) + .Process(this->Vector_CircleMaxAngle) + .Process(this->Vector_CircleMinAngle) + .Process(this->Vector_TargetOffsetFMin) + .Process(this->Vector_TargetOffsetFMax) + .Process(this->Vector_TargetOffsetLMin) + .Process(this->Vector_TargetOffsetLMax) + .Process(this->Vector_TargetOffsetHMin) + .Process(this->Vector_TargetOffsetHMax) + .Process(this->Vector_ArcRandomHeightMin) + .Process(this->Vector_ArcRandomHeightMax) + .Process(this->Vector_ArcRandomRotationMin) + .Process(this->Vector_ArcRandomRotationMax) + .Process(this->Vector_RandomSpeedMin) + .Process(this->Vector_RandomSpeedMax) + .Process(this->Vector_OriginMoveTo) + .Process(this->Vector_OriginGrowRate) + .Process(this->Vector_OriginTargetFLH) + .Process(this->Vector_OriginInitialSpeed) + .Process(this->Vector_OriginAcceleration) + .Process(this->Vector_OriginMaxSpeed) + .Process(this->Vector_OriginMinSpeed) + .Process(this->Vector_OriginTargetOffsetFMin) + .Process(this->Vector_OriginTargetOffsetFMax) + .Process(this->Vector_OriginTargetOffsetLMin) + .Process(this->Vector_OriginTargetOffsetLMax) + .Process(this->Vector_OriginTargetOffsetHMin) + .Process(this->Vector_OriginTargetOffsetHMax) + .Process(this->Vector_OriginReachTarget) + .Process(this->Vector_OriginArcHeight) + .Process(this->Vector_OriginCircleRadius) + .Process(this->Vector_OriginCircleSpeed) + .Process(this->Vector_OriginCircleAnglePerStep) + .Process(this->Vector_OriginLissajous) + .Process(this->Vector_OriginCircleRadiusGrow) + .Process(this->Vector_OriginCircleMaxRadius) + .Process(this->Vector_OriginCircleMinRadius) + .Process(this->Vector_OriginCircleEndOnMaxRadius) + .Process(this->Vector_OriginCircleEndOnMinRadius) + .Process(this->Vector_OriginNormalVector) + .Process(this->Vector_OriginNormalFAnglePerStep) + .Process(this->Vector_OriginNormalLAnglePerStep) + .Process(this->Vector_OriginNormalHAnglePerStep) + .Process(this->Vector_OriginAllowedTilt) + .Process(this->Vector_OriginCircleOffset) + .Process(this->Vector_OriginOrigin) + .Process(this->Vector_OriginOriginFLH) + .Process(this->Vector_NormalFAngleRMin) + .Process(this->Vector_NormalFAngleRMax) + .Process(this->Vector_NormalFAngleRMin2) + .Process(this->Vector_NormalFAngleRMax2) + .Process(this->Vector_NormalLAngleRMin) + .Process(this->Vector_NormalLAngleRMax) + .Process(this->Vector_NormalLAngleRMin2) + .Process(this->Vector_NormalLAngleRMax2) + .Process(this->Vector_NormalHAngleRMin) + .Process(this->Vector_NormalHAngleRMax) + .Process(this->Vector_NormalHAngleRMin2) + .Process(this->Vector_NormalHAngleRMax2) + .Process(this->Vector_OriginNormalFAngleRMin) + .Process(this->Vector_OriginNormalFAngleRMax) + .Process(this->Vector_OriginNormalFAngleRMin2) + .Process(this->Vector_OriginNormalFAngleRMax2) + .Process(this->Vector_OriginNormalLAngleRMin) + .Process(this->Vector_OriginNormalLAngleRMax) + .Process(this->Vector_OriginNormalLAngleRMin2) + .Process(this->Vector_OriginNormalLAngleRMax2) + .Process(this->Vector_OriginNormalHAngleRMin) + .Process(this->Vector_OriginNormalHAngleRMax) + .Process(this->Vector_OriginNormalHAngleRMin2) + .Process(this->Vector_OriginNormalHAngleRMax2) .Process(this->Groups) ; } @@ -371,6 +645,30 @@ namespace detail return false; } + + template <> + inline bool read(VectorOrigin& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + auto str = parser.value(); + + if (!_strcmpi(str, "Self")) + value = VectorOrigin::Self; + else if (!_strcmpi(str, "Launcher")) + value = VectorOrigin::Launcher; + else if (!_strcmpi(str, "Target")) + value = VectorOrigin::Target; + else if (!_strcmpi(str, "Source")) + value = VectorOrigin::Source; + else + value = VectorOrigin::Self; + + return true; + } + + return false; + } } // AEAttachInfoTypeClass diff --git a/src/New/Type/AttachEffectTypeClass.h b/src/New/Type/AttachEffectTypeClass.h index 7d5a09243a..58d02f8782 100644 --- a/src/New/Type/AttachEffectTypeClass.h +++ b/src/New/Type/AttachEffectTypeClass.h @@ -36,6 +36,14 @@ enum class ExpireWeaponCondition : unsigned char MAKE_ENUM_FLAGS(ExpireWeaponCondition); +enum class VectorOrigin : int +{ + Self = 0, + Launcher = 1, + Target = 2, + Source = 3 +}; + class AttachEffectTypeClass final : public Enumerable { static std::unordered_map> GroupsMap; @@ -67,6 +75,7 @@ class AttachEffectTypeClass final : public Enumerable Valueable ExpireWeapon_TriggerOn; Valueable ExpireWeapon_CumulativeOnlyOnce; Valueable ExpireWeapon_UseInvokerAsOwner; + ValueableVector Next; Nullable Tint_Color; Valueable Tint_Intensity; Valueable Tint_VisibleToHouses; @@ -102,6 +111,141 @@ class AttachEffectTypeClass final : public Enumerable Valueable Unkillable; ValueableIdx LaserTrail_Type; + Valueable Vector_TimeStep; + Valueable Vector_DisabledFrames; + Valueable Vector_SyncFacing; + Valueable Vector_OriginIsOnWorld; + Valueable Vector_OriginIsOnBody; + Valueable Vector_Origin; + Nullable Vector_OriginFLH; + Nullable Vector_OriginNoUpdate; + Valueable Vector_Force; + Valueable Vector_Freeze; + Valueable Vector_AllowedTilt; + Nullable Vector_NormalVector; + Valueable Vector_NormalRandomF; + Valueable Vector_NormalRandomL; + Valueable Vector_NormalRandomH; + Valueable Vector_NormalRandomFMin; + Valueable Vector_NormalRandomFMax; + Valueable Vector_NormalRandomLMin; + Valueable Vector_NormalRandomLMax; + Valueable Vector_NormalRandomHMin; + Valueable Vector_NormalRandomHMax; + Valueable Vector_NormalFAnglePerStep; + Valueable Vector_NormalLAnglePerStep; + Valueable Vector_NormalHAnglePerStep; + Valueable Vector_MoveTo; + Nullable Vector_GrowRate; + Valueable Vector_AnglePerStep; + Valueable Vector_CircleRadius; + Valueable Vector_CircleSpeed; + Valueable Vector_CircleSpeedAcceleration; + Valueable Vector_CircleMaxSpeed; + Valueable Vector_CircleMinSpeed; + Valueable Vector_CircleAnglePerStep; + Valueable Vector_CircleAngleAcceleration; + Nullable Vector_CircleOrigin; + Valueable Vector_AllowOriginTilt; + Valueable Vector_CircleRadiusGrow; + Valueable Vector_CircleMaxRadius; + Valueable Vector_CircleMinRadius; + Valueable Vector_CircleEndOnMaxRadius; + Valueable Vector_CircleEndOnMinRadius; + Nullable Vector_TargetFLH; + Valueable Vector_ReachTarget; + Valueable Vector_ReachTargetEarlyEnd; + Valueable Vector_ArcHeight; + Valueable Vector_ArcPeakPercent; + Valueable Vector_ArcPeakRandomPercent; + Valueable Vector_ArcPeakRandomPercentMin; + Valueable Vector_ArcPeakRandomPercentMax; + Valueable Vector_ArcRotation; + Valueable Vector_InitialSpeed; + Valueable Vector_MaxSpeed; + Valueable Vector_MinSpeed; + Valueable Vector_Acceleration; + Valueable Vector_AllowFallingDestroy; + Valueable Vector_FallingDestroyHeight; + Valueable Vector_AffectTechno; + Valueable Vector_AffectBullets; + Valueable Vector_CircleRandomRadiusMin; + Valueable Vector_CircleRandomRadiusMax; + Valueable Vector_CircleRandomAngleMin; + Valueable Vector_CircleRandomAngleMax; + Valueable Vector_CircleRandomAngleMin2; + Valueable Vector_CircleRandomAngleMax2; + Valueable Vector_CircleMaxAngle; + Valueable Vector_CircleMinAngle; + Valueable Vector_TargetOffsetFMin; + Valueable Vector_TargetOffsetFMax; + Valueable Vector_TargetOffsetLMin; + Valueable Vector_TargetOffsetLMax; + Valueable Vector_TargetOffsetHMin; + Valueable Vector_TargetOffsetHMax; + Valueable Vector_ArcRandomHeightMin; + Valueable Vector_ArcRandomHeightMax; + Valueable Vector_ArcRandomRotationMin; + Valueable Vector_ArcRandomRotationMax; + Valueable Vector_RandomSpeedMin; + Valueable Vector_RandomSpeedMax; + Nullable Vector_OriginMoveTo; + Nullable Vector_OriginGrowRate; + Nullable Vector_OriginTargetFLH; + Valueable Vector_OriginInitialSpeed; + Valueable Vector_OriginAcceleration; + Valueable Vector_OriginMaxSpeed; + Valueable Vector_OriginMinSpeed; + Valueable Vector_OriginTargetOffsetFMin; + Valueable Vector_OriginTargetOffsetFMax; + Valueable Vector_OriginTargetOffsetLMin; + Valueable Vector_OriginTargetOffsetLMax; + Valueable Vector_OriginTargetOffsetHMin; + Valueable Vector_OriginTargetOffsetHMax; + Valueable Vector_OriginReachTarget; + Valueable Vector_OriginArcHeight; + Valueable Vector_OriginCircleRadius; + Valueable Vector_OriginCircleSpeed; + Valueable Vector_OriginCircleAnglePerStep; + Valueable Vector_OriginLissajous; + Valueable Vector_OriginCircleRadiusGrow; + Valueable Vector_OriginCircleMaxRadius; + Valueable Vector_OriginCircleMinRadius; + Valueable Vector_OriginCircleEndOnMaxRadius; + Valueable Vector_OriginCircleEndOnMinRadius; + Nullable Vector_OriginNormalVector; + Valueable Vector_OriginNormalFAnglePerStep; + Valueable Vector_OriginNormalLAnglePerStep; + Valueable Vector_OriginNormalHAnglePerStep; + Valueable Vector_OriginAllowedTilt; + Nullable Vector_OriginCircleOffset; + Valueable Vector_OriginOrigin; + Nullable Vector_OriginOriginFLH; + Valueable Vector_NormalFAngleRMin; + Valueable Vector_NormalFAngleRMax; + Valueable Vector_NormalFAngleRMin2; + Valueable Vector_NormalFAngleRMax2; + Valueable Vector_NormalLAngleRMin; + Valueable Vector_NormalLAngleRMax; + Valueable Vector_NormalLAngleRMin2; + Valueable Vector_NormalLAngleRMax2; + Valueable Vector_NormalHAngleRMin; + Valueable Vector_NormalHAngleRMax; + Valueable Vector_NormalHAngleRMin2; + Valueable Vector_NormalHAngleRMax2; + Valueable Vector_OriginNormalFAngleRMin; + Valueable Vector_OriginNormalFAngleRMax; + Valueable Vector_OriginNormalFAngleRMin2; + Valueable Vector_OriginNormalFAngleRMax2; + Valueable Vector_OriginNormalLAngleRMin; + Valueable Vector_OriginNormalLAngleRMax; + Valueable Vector_OriginNormalLAngleRMin2; + Valueable Vector_OriginNormalLAngleRMax2; + Valueable Vector_OriginNormalHAngleRMin; + Valueable Vector_OriginNormalHAngleRMax; + Valueable Vector_OriginNormalHAngleRMin2; + Valueable Vector_OriginNormalHAngleRMax2; + std::vector Groups; AttachEffectTypeClass(const char* const pTitle) : Enumerable(pTitle) @@ -131,6 +275,7 @@ class AttachEffectTypeClass final : public Enumerable , ExpireWeapon_TriggerOn { ExpireWeaponCondition::Expire } , ExpireWeapon_CumulativeOnlyOnce { false } , ExpireWeapon_UseInvokerAsOwner { false } + , Next {} , Tint_Color {} , Tint_Intensity { 0.0 } , Tint_VisibleToHouses { AffectedHouse::All } @@ -165,6 +310,140 @@ class AttachEffectTypeClass final : public Enumerable , DisableWeapons { false } , Unkillable { false } , LaserTrail_Type { -1 } + , Vector_TimeStep { 1 } + , Vector_DisabledFrames { 0 } + , Vector_SyncFacing { true } + , Vector_OriginIsOnWorld { false } + , Vector_OriginIsOnBody { false } + , Vector_Origin { VectorOrigin::Self } + , Vector_OriginFLH {} + , Vector_OriginNoUpdate {} + , Vector_Force { true } + , Vector_Freeze { false } + , Vector_AllowedTilt { false } + , Vector_NormalVector {} + , Vector_NormalRandomF { 0.0 } + , Vector_NormalRandomL { 0.0 } + , Vector_NormalRandomH { 0.0 } + , Vector_NormalRandomFMin { 0.0 } + , Vector_NormalRandomFMax { 0.0 } + , Vector_NormalRandomLMin { 0.0 } + , Vector_NormalRandomLMax { 0.0 } + , Vector_NormalRandomHMin { 0.0 } + , Vector_NormalRandomHMax { 0.0 } + , Vector_NormalFAnglePerStep { 0.0 } + , Vector_NormalLAnglePerStep { 0.0 } + , Vector_NormalHAnglePerStep { 0.0 } + , Vector_MoveTo { CoordStruct::Empty } + , Vector_GrowRate {} + , Vector_AnglePerStep { 0.0 } + , Vector_CircleRadius { -1 } + , Vector_CircleSpeed { 0 } + , Vector_CircleSpeedAcceleration { 0 } + , Vector_CircleMaxSpeed { 0 } + , Vector_CircleMinSpeed { 0 } + , Vector_CircleAnglePerStep { 0.0 } + , Vector_CircleAngleAcceleration { 0.0 } + , Vector_CircleOrigin {} + , Vector_AllowOriginTilt { false } + , Vector_CircleRadiusGrow { 0 } + , Vector_CircleMaxRadius { 0 } + , Vector_CircleMinRadius { 0 } + , Vector_CircleEndOnMaxRadius { false } + , Vector_CircleEndOnMinRadius { false } + , Vector_TargetFLH {} + , Vector_ReachTarget { false } + , Vector_ReachTargetEarlyEnd { 0 } + , Vector_ArcHeight { 0 } + , Vector_ArcPeakPercent { 0.0 } + , Vector_ArcPeakRandomPercent { 0.0 } + , Vector_ArcPeakRandomPercentMin { 0.0 } + , Vector_ArcPeakRandomPercentMax { 0.0 } + , Vector_ArcRotation { 0.0 } + , Vector_InitialSpeed { -1 } + , Vector_MaxSpeed { -1 } + , Vector_MinSpeed { -1 } + , Vector_Acceleration { 0 } + , Vector_AllowFallingDestroy { false } + , Vector_FallingDestroyHeight { 0 } + , Vector_AffectTechno { false } + , Vector_AffectBullets { true } + , Vector_CircleRandomRadiusMin { -1 } + , Vector_CircleRandomRadiusMax { -1 } + , Vector_CircleRandomAngleMin { -1 } + , Vector_CircleRandomAngleMax { -1 } + , Vector_CircleRandomAngleMin2 { -1 } + , Vector_CircleRandomAngleMax2 { -1 } + , Vector_CircleMaxAngle { 0.0 } + , Vector_CircleMinAngle { 0.0 } + , Vector_TargetOffsetFMin { 0 } + , Vector_TargetOffsetFMax { 0 } + , Vector_TargetOffsetLMin { 0 } + , Vector_TargetOffsetLMax { 0 } + , Vector_TargetOffsetHMin { 0 } + , Vector_TargetOffsetHMax { 0 } + , Vector_ArcRandomHeightMin { 0 } + , Vector_ArcRandomHeightMax { 0 } + , Vector_ArcRandomRotationMin { 0.0 } + , Vector_ArcRandomRotationMax { 0.0 } + , Vector_RandomSpeedMin { -1 } + , Vector_RandomSpeedMax { -1 } + , Vector_OriginMoveTo {} + , Vector_OriginGrowRate {} + , Vector_OriginTargetFLH {} + , Vector_OriginInitialSpeed { -1 } + , Vector_OriginAcceleration { 0 } + , Vector_OriginMaxSpeed { -1 } + , Vector_OriginMinSpeed { -1 } + , Vector_OriginTargetOffsetFMin { 0 } + , Vector_OriginTargetOffsetFMax { 0 } + , Vector_OriginTargetOffsetLMin { 0 } + , Vector_OriginTargetOffsetLMax { 0 } + , Vector_OriginTargetOffsetHMin { 0 } + , Vector_OriginTargetOffsetHMax { 0 } + , Vector_OriginReachTarget { false } + , Vector_OriginArcHeight { 0 } + , Vector_OriginCircleRadius { -1 } + , Vector_OriginCircleSpeed { 0 } + , Vector_OriginCircleAnglePerStep { 0.0 } + , Vector_OriginLissajous { false } + , Vector_OriginCircleRadiusGrow { 0 } + , Vector_OriginCircleMaxRadius { 0 } + , Vector_OriginCircleMinRadius { 0 } + , Vector_OriginCircleEndOnMaxRadius { false } + , Vector_OriginCircleEndOnMinRadius { false } + , Vector_OriginNormalVector {} + , Vector_OriginNormalFAnglePerStep { 0.0 } + , Vector_OriginNormalLAnglePerStep { 0.0 } + , Vector_OriginNormalHAnglePerStep { 0.0 } + , Vector_OriginAllowedTilt { false } + , Vector_OriginCircleOffset {} + , Vector_OriginOrigin { VectorOrigin::Self } + , Vector_OriginOriginFLH {} + , Vector_NormalFAngleRMin { 0.0 } + , Vector_NormalFAngleRMax { 0.0 } + , Vector_NormalFAngleRMin2 { 0.0 } + , Vector_NormalFAngleRMax2 { 0.0 } + , Vector_NormalLAngleRMin { 0.0 } + , Vector_NormalLAngleRMax { 0.0 } + , Vector_NormalLAngleRMin2 { 0.0 } + , Vector_NormalLAngleRMax2 { 0.0 } + , Vector_NormalHAngleRMin { 0.0 } + , Vector_NormalHAngleRMax { 0.0 } + , Vector_NormalHAngleRMin2 { 0.0 } + , Vector_NormalHAngleRMax2 { 0.0 } + , Vector_OriginNormalFAngleRMin { 0.0 } + , Vector_OriginNormalFAngleRMax { 0.0 } + , Vector_OriginNormalFAngleRMin2 { 0.0 } + , Vector_OriginNormalFAngleRMax2 { 0.0 } + , Vector_OriginNormalLAngleRMin { 0.0 } + , Vector_OriginNormalLAngleRMax { 0.0 } + , Vector_OriginNormalLAngleRMin2 { 0.0 } + , Vector_OriginNormalLAngleRMax2 { 0.0 } + , Vector_OriginNormalHAngleRMin { 0.0 } + , Vector_OriginNormalHAngleRMax { 0.0 } + , Vector_OriginNormalHAngleRMin2 { 0.0 } + , Vector_OriginNormalHAngleRMax2 { 0.0 } , Groups {} {}; @@ -173,6 +452,20 @@ class AttachEffectTypeClass final : public Enumerable return this->Tint_Color.isset() || this->Tint_Intensity != 0.0; } + bool HasVector() const + { + return static_cast(this->Vector_MoveTo) != CoordStruct::Empty + || this->Vector_TargetFLH.isset() + || this->Vector_Freeze + || this->Vector_ReachTarget + || this->Vector_CircleRadius > 0 + || this->Vector_CircleSpeed != 0 + || this->Vector_CircleAnglePerStep > 0.0 + || (this->Vector_CircleRandomRadiusMax > this->Vector_CircleRandomRadiusMin) + || (this->Vector_CircleRandomAngleMax > this->Vector_CircleRandomAngleMin) + || this->Vector_OriginMoveTo.isset(); + } + bool HasGroup(const std::string& groupID) const; bool HasGroups(const std::vector& groupIDs, bool requireAll) const;