diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index c8a07539c3..d466517cb0 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -61,10 +61,12 @@
-
+
-
-
+
+
+
+
@@ -241,10 +243,11 @@
-
-
-
+
+
+
+
diff --git a/src/Ext/Bullet/AdditionalWarheads.cpp b/src/Ext/Bullet/AdditionalWarheads.cpp
new file mode 100644
index 0000000000..7ac92c02e5
--- /dev/null
+++ b/src/Ext/Bullet/AdditionalWarheads.cpp
@@ -0,0 +1,713 @@
+#include "Body.h"
+
+#include
+
+#include
+
+// A rectangular shape with a custom width from the current frame to the next frame in length.
+std::vector BulletExt::ExtData::GetCellsInProximityRadius()
+{
+ const auto pBullet = this->OwnerObject();
+ const auto pTraj = this->Trajectory.get();
+
+ // Seems like the y-axis is reversed, but it's okay.
+ const auto walkCoord = pTraj ? Vector2D{ pTraj->MovingVelocity.X, pTraj->MovingVelocity.Y } : Vector2D{ pBullet->Velocity.X, pBullet->Velocity.Y };
+ const double walkDistance = walkCoord.Magnitude();
+ const auto radius = this->TypeExtData->ProximityRadius.Get();
+ const auto thisCell = CellClass::Coord2Cell(pBullet->Location);
+
+ // Special case of zero speed
+ if (walkDistance <= BulletExt::Epsilon)
+ {
+ const double range = radius / static_cast(Unsorted::LeptonsPerCell);
+ std::vector cirCellClass;
+ const auto roundRange = static_cast(range + 0.99);
+ cirCellClass.reserve(roundRange * roundRange);
+
+ for (CellSpreadEnumerator checkCell(roundRange); checkCell; ++checkCell)
+ {
+ if (const auto pCirCell = MapClass::Instance.TryGetCellAt(*checkCell + thisCell))
+ cirCellClass.push_back(pCirCell);
+ }
+
+ return cirCellClass;
+ }
+
+ const double sideMult = radius / walkDistance;
+
+ const CoordStruct cor1Coord { static_cast(walkCoord.Y * sideMult), static_cast((-walkCoord.X) * sideMult), 0 };
+ const CoordStruct cor4Coord { static_cast((-walkCoord.Y) * sideMult), static_cast(walkCoord.X * sideMult), 0 };
+
+ auto cor1Cell = CellClass::Coord2Cell(pBullet->Location + cor1Coord);
+ auto cor4Cell = CellClass::Coord2Cell(pBullet->Location + cor4Coord);
+
+ const auto off1Cell = cor1Cell - thisCell;
+ const auto off4Cell = cor4Cell - thisCell;
+
+ const double predictRatio = (walkDistance + radius) / walkDistance;
+ const CoordStruct predictCoord { static_cast(walkCoord.X * predictRatio), static_cast(walkCoord.Y * predictRatio), 0 };
+ const auto nextCell = CellClass::Coord2Cell(pBullet->Location + predictCoord);
+
+ auto cor2Cell = nextCell + off1Cell;
+ auto cor3Cell = nextCell + off4Cell;
+
+ // Arrange the vertices of the rectangle in order from bottom to top.
+ int cornerIndex = 0;
+ CellStruct corner[4] = { cor1Cell, cor2Cell, cor3Cell, cor4Cell };
+
+ for (int i = 1; i < 4; ++i)
+ {
+ if (corner[cornerIndex].Y > corner[i].Y)
+ cornerIndex = i;
+ }
+
+ cor1Cell = corner[cornerIndex];
+ cornerIndex = (cornerIndex + 1) % 4;
+ cor2Cell = corner[cornerIndex];
+ cornerIndex = (cornerIndex + 1) % 4;
+ cor3Cell = corner[cornerIndex];
+ cornerIndex = (cornerIndex + 1) % 4;
+ cor4Cell = corner[cornerIndex];
+
+ // Obtain cells through vertices
+ std::vector recCells = BulletExt::GetCellsInRectangle(cor1Cell, cor4Cell, cor2Cell, cor3Cell);
+ std::vector recCellClass;
+ recCellClass.reserve(recCells.size());
+
+ for (const auto& pCells : recCells)
+ {
+ if (const auto pRecCell = MapClass::Instance.TryGetCellAt(pCells))
+ recCellClass.push_back(pRecCell);
+ }
+
+ return recCellClass;
+}
+
+/*!
+ Can ONLY fill RECTANGLE.
+ Record cells in the order of "draw left boundary, draw right boundary, fill middle, and move up one level".
+
+ \param bottomStaCell Starting point vertex, located at the lowest point of the Y-axis.
+ \param leftMidCell The vertex in the middle of the left path.
+ \param rightMidCell The vertex in the middle of the right path.
+ \param topEndCell The endpoint vertex, located at the highest point on the Y-axis.
+
+ \returns A container that records all the cells inside, rounded outward.
+
+ \author CrimRecya
+*/
+std::vector BulletExt::GetCellsInRectangle(const CellStruct bottomStaCell, const CellStruct leftMidCell, const CellStruct rightMidCell, const CellStruct topEndCell)
+{
+ std::vector recCells;
+ const int cellNums = (std::abs(topEndCell.Y - bottomStaCell.Y) + 1) * (std::abs(rightMidCell.X - leftMidCell.X) + 1);
+ recCells.reserve(cellNums);
+ recCells.push_back(bottomStaCell);
+
+ if (bottomStaCell == leftMidCell || bottomStaCell == rightMidCell) // A straight line
+ {
+ auto middleCurCell = bottomStaCell;
+
+ const auto middleTheDist = topEndCell - bottomStaCell;
+ const CellStruct middleTheUnit { static_cast(Math::sgn(middleTheDist.X)), static_cast(Math::sgn(middleTheDist.Y)) };
+ const CellStruct middleThePace { static_cast(middleTheDist.X * middleTheUnit.X), static_cast(middleTheDist.Y * middleTheUnit.Y) };
+ short mTheCurN = static_cast((middleThePace.Y - middleThePace.X) / 2);
+
+ while (middleCurCell != topEndCell)
+ {
+ if (mTheCurN > 0)
+ {
+ mTheCurN -= middleThePace.X;
+ middleCurCell.Y += middleTheUnit.Y;
+ recCells.push_back(middleCurCell);
+ }
+ else if (mTheCurN < 0)
+ {
+ mTheCurN += middleThePace.Y;
+ middleCurCell.X += middleTheUnit.X;
+ recCells.push_back(middleCurCell);
+ }
+ else
+ {
+ mTheCurN += middleThePace.Y - middleThePace.X;
+ middleCurCell.X += middleTheUnit.X;
+ recCells.push_back(middleCurCell);
+ middleCurCell.X -= middleTheUnit.X;
+ middleCurCell.Y += middleTheUnit.Y;
+ recCells.push_back(middleCurCell);
+ middleCurCell.X += middleTheUnit.X;
+ recCells.push_back(middleCurCell);
+ }
+ }
+ }
+ else // Complete rectangle
+ {
+ auto leftCurCell = bottomStaCell;
+ auto rightCurCell = bottomStaCell;
+ auto middleCurCell = bottomStaCell;
+
+ bool leftNext = false;
+ bool rightNext = false;
+ bool leftSkip = false;
+ bool rightSkip = false;
+ bool leftContinue = false;
+ bool rightContinue = false;
+
+ const auto left1stDist = leftMidCell - bottomStaCell;
+ const CellStruct left1stUnit { static_cast(Math::sgn(left1stDist.X)), static_cast(Math::sgn(left1stDist.Y)) };
+ const CellStruct left1stPace { static_cast(left1stDist.X * left1stUnit.X), static_cast(left1stDist.Y * left1stUnit.Y) };
+ short left1stCurN = static_cast((left1stPace.Y - left1stPace.X) / 2);
+
+ const auto left2ndDist = topEndCell - leftMidCell;
+ const CellStruct left2ndUnit { static_cast(Math::sgn(left2ndDist.X)), static_cast(Math::sgn(left2ndDist.Y)) };
+ const CellStruct left2ndPace { static_cast(left2ndDist.X * left2ndUnit.X), static_cast(left2ndDist.Y * left2ndUnit.Y) };
+ short left2ndCurN = static_cast((left2ndPace.Y - left2ndPace.X) / 2);
+
+ const auto right1stDist = rightMidCell - bottomStaCell;
+ const CellStruct right1stUnit { static_cast(Math::sgn(right1stDist.X)), static_cast(Math::sgn(right1stDist.Y)) };
+ const CellStruct right1stPace { static_cast(right1stDist.X * right1stUnit.X), static_cast(right1stDist.Y * right1stUnit.Y) };
+ short right1stCurN = static_cast((right1stPace.Y - right1stPace.X) / 2);
+
+ const auto right2ndDist = topEndCell - rightMidCell;
+ const CellStruct right2ndUnit { static_cast(Math::sgn(right2ndDist.X)), static_cast(Math::sgn(right2ndDist.Y)) };
+ const CellStruct right2ndPace { static_cast(right2ndDist.X * right2ndUnit.X), static_cast(right2ndDist.Y * right2ndUnit.Y) };
+ short right2ndCurN = static_cast((right2ndPace.Y - right2ndPace.X) / 2);
+
+ while (leftCurCell != topEndCell || rightCurCell != topEndCell)
+ {
+ while (leftCurCell != topEndCell) // Left
+ {
+ if (!leftNext) // Bottom Left Side
+ {
+ if (left1stCurN > 0)
+ {
+ left1stCurN -= left1stPace.X;
+ leftCurCell.Y += left1stUnit.Y;
+
+ if (leftCurCell == leftMidCell)
+ {
+ leftNext = true;
+ }
+ else
+ {
+ recCells.push_back(leftCurCell);
+ break;
+ }
+ }
+ else
+ {
+ left1stCurN += left1stPace.Y;
+ leftCurCell.X += left1stUnit.X;
+
+ if (leftCurCell == leftMidCell)
+ {
+ leftNext = true;
+ leftSkip = true;
+ }
+ }
+ }
+ else // Top Left Side
+ {
+ if (left2ndCurN >= 0)
+ {
+ if (leftSkip)
+ {
+ leftSkip = false;
+ left2ndCurN -= left2ndPace.X;
+ leftCurCell.Y += left2ndUnit.Y;
+ }
+ else
+ {
+ leftContinue = true;
+ break;
+ }
+ }
+ else
+ {
+ left2ndCurN += left2ndPace.Y;
+ leftCurCell.X += left2ndUnit.X;
+ }
+ }
+
+ if (leftCurCell != rightCurCell) // Avoid double counting cells.
+ recCells.push_back(leftCurCell);
+ }
+
+ while (rightCurCell != topEndCell) // Right
+ {
+ if (!rightNext) // Bottom Right Side
+ {
+ if (right1stCurN > 0)
+ {
+ right1stCurN -= right1stPace.X;
+ rightCurCell.Y += right1stUnit.Y;
+
+ if (rightCurCell == rightMidCell)
+ {
+ rightNext = true;
+ }
+ else
+ {
+ recCells.push_back(rightCurCell);
+ break;
+ }
+ }
+ else
+ {
+ right1stCurN += right1stPace.Y;
+ rightCurCell.X += right1stUnit.X;
+
+ if (rightCurCell == rightMidCell)
+ {
+ rightNext = true;
+ rightSkip = true;
+ }
+ }
+ }
+ else // Top Right Side
+ {
+ if (right2ndCurN >= 0)
+ {
+ if (rightSkip)
+ {
+ rightSkip = false;
+ right2ndCurN -= right2ndPace.X;
+ rightCurCell.Y += right2ndUnit.Y;
+ }
+ else
+ {
+ rightContinue = true;
+ break;
+ }
+ }
+ else
+ {
+ right2ndCurN += right2ndPace.Y;
+ rightCurCell.X += right2ndUnit.X;
+ }
+ }
+
+ if (rightCurCell != leftCurCell) // Avoid double counting cells.
+ recCells.push_back(rightCurCell);
+ }
+
+ middleCurCell = leftCurCell;
+ middleCurCell.X += 1;
+
+ while (middleCurCell.X < rightCurCell.X) // Center
+ {
+ recCells.push_back(middleCurCell);
+ middleCurCell.X += 1;
+ }
+
+ if (leftContinue) // Continue Top Left Side
+ {
+ leftContinue = false;
+ left2ndCurN -= left2ndPace.X;
+ leftCurCell.Y += left2ndUnit.Y;
+ recCells.push_back(leftCurCell);
+ }
+
+ if (rightContinue) // Continue Top Right Side
+ {
+ rightContinue = false;
+ right2ndCurN -= right2ndPace.X;
+ rightCurCell.Y += right2ndUnit.Y;
+ recCells.push_back(rightCurCell);
+ }
+ }
+ }
+
+ return recCells;
+}
+
+bool BulletExt::ExtData::CheckThroughAndSubjectInCell(CellClass* pCell, HouseClass* pOwner)
+{
+ const auto pTarget = this->OwnerObject()->Target;
+ const auto pType = this->TypeExtData;
+
+ for (auto pObject = pCell->GetContent(); pObject; pObject = pObject->NextObject)
+ {
+ const auto pTechno = abstract_cast(pObject);
+
+ // Non technos and not target friendly forces will be excluded
+ if (!pTechno || (pOwner && pOwner->IsAlliedWith(pTechno->Owner) && pTechno != pTarget))
+ continue;
+
+ const auto absType = pTechno->WhatAmI();
+
+ // Check building obstacles
+ if (absType == AbstractType::Building)
+ {
+ const auto pBuilding = static_cast(pTechno);
+
+ if (pBuilding->Type->InvisibleInGame)
+ continue;
+
+ if (pBuilding->IsStrange() ? !pType->ThroughVehicles : !pType->ThroughBuilding)
+ {
+ this->ExtraCheck = pTechno;
+ return true;
+ }
+ }
+
+ // Check unit obstacles
+ if (!pType->ThroughVehicles && (absType == AbstractType::Unit || absType == AbstractType::Aircraft))
+ {
+ this->ExtraCheck = pTechno;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void BulletExt::ExtData::CalculateNewDamage()
+{
+ const auto pBullet = this->OwnerObject();
+ const double ratio = this->TypeExtData->DamageCountAttenuation.Get();
+
+ // Calculate the attenuation damage under three different scenarios
+ if (ratio != 1.0)
+ {
+ // If the ratio is not 0, the lowest damage will be retained
+ if (ratio)
+ {
+ BulletExt::SetNewDamage(pBullet->Health, ratio);
+ BulletExt::SetNewDamage(this->ProximityDamage, ratio);
+ BulletExt::SetNewDamage(this->PassDetonateDamage, ratio);
+ }
+ else
+ {
+ pBullet->Health = 0;
+ this->ProximityDamage = 0;
+ this->PassDetonateDamage = 0;
+ }
+ }
+}
+
+void BulletExt::ExtData::PassWithDetonateAt()
+{
+ if (!this->PassDetonateTimer.Completed())
+ return;
+
+ const auto pBullet = this->OwnerObject();
+ const auto pType = this->TypeExtData;
+ auto pWH = pType->PassDetonateWarhead.Get();
+
+ if (!pWH)
+ pWH = pBullet->WH;
+
+ this->PassDetonateTimer.Start(pType->PassDetonateDelay);
+ auto detonateCoords = pBullet->Location;
+
+ // Whether to detonate at ground level?
+ if (pType->PassDetonateLocal)
+ detonateCoords.Z = MapClass::Instance.GetCellFloorHeight(detonateCoords);
+
+ const auto pFirer = pBullet->Owner;
+ const auto pOwner = pFirer ? pFirer->Owner : BulletExt::ExtMap.Find(pBullet)->FirerHouse;
+ const int damage = this->GetTrueDamage(this->PassDetonateDamage, false);
+ WarheadTypeExt::DetonateAt(pWH, detonateCoords, pBullet->Owner, damage, pOwner);
+ this->CalculateNewDamage();
+}
+
+// Select suitable targets and choose the closer targets then attack each target only once.
+void BulletExt::ExtData::PrepareForDetonateAt()
+{
+ const auto pType = this->TypeExtData;
+ const auto pBullet = this->OwnerObject();
+ const auto pFirer = pBullet->Owner;
+ const auto pOwner = pFirer ? pFirer->Owner : BulletExt::ExtMap.Find(pBullet)->FirerHouse;
+ const auto radius = pType->ProximityRadius.Get();
+ auto pWH = pType->ProximityWarhead.Get();
+
+ if (!pWH)
+ pWH = pBullet->WH;
+
+ const auto pWHExt = WarheadTypeExt::ExtMap.Find(pWH);
+
+ // Step 1: Find valid targets on the ground within range.
+ std::vector recCellClass = this->GetCellsInProximityRadius();
+
+ const auto pTraj = this->Trajectory.get();
+ const auto velocityCrd = BulletExt::Vector2Coord(pTraj ? pTraj->MovingVelocity : pBullet->Velocity);
+ const double velocity = pTraj ? pTraj->MovingSpeed : pBullet->Velocity.Magnitude();
+ const auto pTarget = pBullet->Target;
+
+ std::vector validTechnos;
+ validTechnos.reserve(recCellClass.size() * 2);
+
+ auto checkCellContent = [pType, pBullet, pTarget, pOwner, radius, velocity, pWHExt,
+ &velocityCrd, &validTechnos](ObjectClass* pFirstObject)
+ {
+ for (auto pObject = pFirstObject; pObject; pObject = pObject->NextObject)
+ {
+ const auto pTechno = abstract_cast(pObject);
+
+ if (!pTechno || BulletExt::CheckTechnoIsInvalid(pTechno))
+ continue;
+
+ // Not directly harming friendly forces
+ if (!pType->ProximityAllies && pOwner && pOwner->IsAlliedWith(pTechno->Owner) && pTechno != pTarget)
+ continue;
+
+ if (pTechno->IsBeingWarpedOut() || !pWHExt->IsHealthInThreshold(pTechno))
+ continue;
+
+ const bool isBuilding = pTechno->WhatAmI() == AbstractType::Building;
+
+ if (isBuilding && static_cast(pTechno)->Type->InvisibleInGame)
+ continue;
+
+ // Check distance within the range of half capsule shape
+ const auto targetCrd = pTechno->GetCoords();
+ const auto distanceCrd = targetCrd - pBullet->Location;
+
+ // Should be in front of the bullet's current position
+ if (distanceCrd * velocityCrd < 0)
+ continue;
+
+ int distanceOffset = 0;
+
+ // Building type have an extra bonus to distance (0x5F6403)
+ if (isBuilding)
+ {
+ const auto pBldType = static_cast(pTechno)->Type;
+ distanceOffset = 64 * (pBldType->GetFoundationHeight(false) + pBldType->GetFoundationWidth());
+ }
+
+ const auto nextDistanceCrd = distanceCrd - velocityCrd;
+
+ // Should be behind the bullet's next frame position
+ if (nextDistanceCrd * velocityCrd > 0)
+ {
+ // Otherwise, at least within the spherical range of future position
+ if (static_cast(nextDistanceCrd.Magnitude()) > (radius + distanceOffset))
+ continue;
+ }
+
+ // Calculate the distance between the point and the line
+ const double distance = (velocity > BulletExt::Epsilon) ? (distanceCrd.CrossProduct(nextDistanceCrd).Magnitude() / velocity) : distanceCrd.Magnitude();
+
+ // Should be in the center cylinder
+ if (distance > (radius + distanceOffset))
+ continue;
+
+ validTechnos.push_back(pTechno);
+ }
+ };
+
+ for (const auto& pRecCell : recCellClass)
+ {
+ checkCellContent(pRecCell->FirstObject);
+
+ if (pRecCell->ContainsBridge())
+ checkCellContent(pRecCell->AltObject);
+ }
+
+ // Step 2: Find valid targets in the air within range if necessary.
+ if (pType->ProximityFlight)
+ {
+ const auto airTracker = &AircraftTrackerClass::Instance;
+ airTracker->FillCurrentVector(MapClass::Instance.GetCellAt(pBullet->Location + velocityCrd * 0.5), Game::F2I((velocity / 2 + radius) / Unsorted::LeptonsPerCell));
+
+ for (auto pTechno = airTracker->Get(); pTechno; pTechno = airTracker->Get())
+ {
+ if (BulletExt::CheckTechnoIsInvalid(pTechno))
+ continue;
+
+ // Not directly harming friendly forces
+ if (!pType->ProximityAllies && pOwner && pOwner->IsAlliedWith(pTechno->Owner) && pTechno != pTarget)
+ continue;
+
+ if (pTechno->IsBeingWarpedOut() || !pWHExt->IsHealthInThreshold(pTechno))
+ continue;
+
+ // Check distance within the range of half capsule shape
+ const auto targetCrd = pTechno->GetCoords();
+ const auto distanceCrd = targetCrd - pBullet->Location;
+
+ // Should be in front of the bullet's current position
+ if (distanceCrd * velocityCrd < 0)
+ continue;
+
+ const auto nextDistanceCrd = distanceCrd - velocityCrd;
+
+ // Should be behind the bullet's next frame position
+ if (nextDistanceCrd * velocityCrd > 0)
+ {
+ // Otherwise, at least within the spherical range of future position
+ if (nextDistanceCrd.Magnitude() > radius)
+ continue;
+ }
+
+ // Calculate the distance between the point and the line
+ const double distance = (velocity > BulletExt::Epsilon) ? (distanceCrd.CrossProduct(nextDistanceCrd).Magnitude() / velocity) : distanceCrd.Magnitude();
+
+ // Should be in the center cylinder
+ if (distance > radius)
+ continue;
+
+ validTechnos.push_back(pTechno);
+ }
+ }
+
+ // Step 3: Record each target without repetition.
+ std::vector casualtyChecked;
+ casualtyChecked.reserve(Math::max(validTechnos.size(), this->Casualty.size()));
+
+ // No impact on firer
+ if (pFirer)
+ this->Casualty[pFirer->UniqueID] = 5;
+
+ // Update Record
+ for (const auto& [ID, remainTime] : this->Casualty)
+ {
+ if (remainTime > 0)
+ this->Casualty[ID] = remainTime - 1;
+ else
+ casualtyChecked.push_back(ID);
+ }
+
+ for (const auto& ID : casualtyChecked)
+ this->Casualty.erase(ID);
+
+ std::vector validTargets;
+ validTargets.reserve(validTechnos.size());
+
+ // checking for duplicate
+ for (const auto& pTechno : validTechnos)
+ {
+ if (!this->Casualty.contains(pTechno->UniqueID))
+ validTargets.push_back(pTechno);
+
+ // Record 5 frames
+ this->Casualty[pTechno->UniqueID] = 5;
+ }
+
+ // Step 4: Detonate warheads in sequence based on distance.
+ const auto targetsSize = validTargets.size();
+
+ if (this->ProximityImpact > 0 && static_cast(targetsSize) > this->ProximityImpact)
+ {
+ std::sort(&validTargets[0], &validTargets[targetsSize],[pBullet](TechnoClass* pTechnoA, TechnoClass* pTechnoB)
+ {
+ const double distanceA = pTechnoA->GetCoords().DistanceFromSquared(pBullet->SourceCoords);
+ const double distanceB = pTechnoB->GetCoords().DistanceFromSquared(pBullet->SourceCoords);
+
+ // Distance priority
+ if (distanceA < distanceB)
+ return true;
+
+ if (distanceA > distanceB)
+ return false;
+
+ return pTechnoA->UniqueID < pTechnoB->UniqueID;
+ });
+ }
+
+ for (const auto& pTechno : validTargets)
+ {
+ // Not effective for the technos following it.
+ if (pTechno == this->ExtraCheck)
+ break;
+
+ // Last chance
+ if (this->ProximityImpact == 1)
+ {
+ this->ExtraCheck = pTechno;
+ break;
+ }
+
+ // Skip technos that are within range but will not obstruct and cannot be passed through
+ const auto absType = pTechno->WhatAmI();
+
+ if (!pType->ThroughVehicles && (absType == AbstractType::Unit || absType == AbstractType::Aircraft))
+ continue;
+
+ if (absType == AbstractType::Building && (static_cast(pTechno)->IsStrange() ? !pType->ThroughVehicles : !pType->ThroughBuilding))
+ continue;
+
+ this->ProximityDetonateAt(pOwner, pTechno);
+
+ // Record the number of times
+ if (this->ProximityImpact > 0)
+ --this->ProximityImpact;
+ }
+}
+
+void BulletExt::ExtData::ProximityDetonateAt(HouseClass* pOwner, TechnoClass* pTarget)
+{
+ const auto pBullet = this->OwnerObject();
+ const auto pType = this->TypeExtData;
+ int damage = this->GetTrueDamage(this->ProximityDamage, false);
+ auto pWH = pType->ProximityWarhead.Get();
+
+ if (!pWH)
+ pWH = pBullet->WH;
+
+ // Choose the method of causing damage
+ if (pType->ProximityDirect)
+ pTarget->ReceiveDamage(&damage, 0, pWH, pBullet->Owner, false, false, pOwner);
+ else if (pType->ProximityMedial)
+ WarheadTypeExt::DetonateAt(pWH, pBullet->Location, pBullet->Owner, damage, pOwner);
+ else
+ WarheadTypeExt::DetonateAt(pWH, pTarget, pBullet->Owner, damage, pOwner);
+
+ this->CalculateNewDamage();
+}
+
+int BulletExt::ExtData::GetTrueDamage(int damage, bool self)
+{
+ if (damage == 0)
+ return 0;
+
+ const auto pType = this->TypeExtData;
+
+ // Calculate damage distance attenuation
+ if (pType->DamageEdgeAttenuation != 1.0)
+ {
+ const double damageMultiplier = this->GetExtraDamageMultiplier();
+ const double calculatedDamage = self ? damage * damageMultiplier : damage * this->FirepowerMult * damageMultiplier;
+ const int signal = Math::sgn(calculatedDamage);
+ damage = static_cast(calculatedDamage);
+
+ // Retain minimal damage
+ if (!damage && pType->DamageEdgeAttenuation > 0.0)
+ damage = signal;
+ }
+ else if (!self)
+ {
+ const double calculatedDamage = damage * this->FirepowerMult;
+ const int signal = Math::sgn(calculatedDamage);
+ damage = static_cast(calculatedDamage);
+
+ // Retain minimal damage
+ if (!damage)
+ damage = signal;
+ }
+
+ return damage;
+}
+
+double BulletExt::ExtData::GetExtraDamageMultiplier()
+{
+ const auto pBullet = this->OwnerObject();
+ const double distance = pBullet->Location.DistanceFrom(pBullet->SourceCoords);
+
+ // Directly use edge value if the distance is too far
+ if (this->AttenuationRange <= static_cast(distance))
+ return this->TypeExtData->DamageEdgeAttenuation;
+
+ // Remove the first cell distance for calculation
+ const double calculateDistance = distance - static_cast(Unsorted::LeptonsPerCell);
+
+ // Directly use original value if the distance is too close
+ if (calculateDistance <= 0.0)
+ return 1.0;
+
+ // this->AttenuationRange > distance > Unsorted::LeptonsPerCell -> deltaRange > 0
+ const double deltaMult = this->TypeExtData->DamageEdgeAttenuation - 1.0;
+ const int deltaRange = this->AttenuationRange - Unsorted::LeptonsPerCell;
+ return 1.0 + deltaMult * (calculateDistance / deltaRange);
+}
diff --git a/src/Ext/Bullet/Body.cpp b/src/Ext/Bullet/Body.cpp
index 915c914317..4d6b533ca3 100644
--- a/src/Ext/Bullet/Body.cpp
+++ b/src/Ext/Bullet/Body.cpp
@@ -14,6 +14,277 @@
BulletExt::ExtContainer BulletExt::ExtMap;
+BulletExt::ExtData::~ExtData()
+{
+ if (this->GroupIndex != -1)
+ {
+ if (const auto pMap = this->TrajectoryGroup)
+ {
+ auto& groupData = (*pMap)[this->TypeExtData->OwnerObject()];
+ auto& vec = groupData.Bullets;
+ vec.erase(std::remove(vec.begin(), vec.end(), this->OwnerObject()->UniqueID), vec.end());
+ groupData.ShouldUpdate = true;
+ }
+ }
+}
+
+void BulletExt::ExtData::InitializeOnUnlimbo()
+{
+ const auto pBullet = this->OwnerObject();
+ const auto pBulletExt = BulletExt::ExtMap.Find(pBullet);
+ const auto pBulletTypeExt = pBulletExt->TypeExtData;
+
+ // Without a target, the game will inevitably crash before, so no need to check here
+ const auto pTarget = pBullet->Target;
+
+ // Due to various ways of firing weapons, the true firer may have already died
+ const auto pFirer = pBullet->Owner;
+
+ // Set additional warhead and weapon count
+ pBulletExt->ProximityImpact = pBulletTypeExt->ProximityImpact;
+
+ // Record the status of the target
+ pBulletExt->TargetIsTechno = (pTarget->AbstractFlags & AbstractFlags::Techno) != AbstractFlags::None;
+ pBulletExt->TargetIsInAir = (pTarget->AbstractFlags & AbstractFlags::Object) ? (static_cast(pTarget)->GetHeight() > Unsorted::CellHeight) : false;
+ int damage = pBullet->Health;
+
+ // Record some information of weapon
+ if (const auto pWeapon = pBullet->WeaponType)
+ {
+ pBulletExt->AttenuationRange = pWeapon->Range;
+
+ if (pBulletTypeExt->ApplyRangeModifiers && pFirer)
+ pBulletExt->AttenuationRange = WeaponTypeExt::GetRangeWithModifiers(pWeapon, pFirer);
+
+ damage = pWeapon->Damage;
+ }
+
+ // Set basic damage
+ pBulletExt->ProximityDamage = pBulletTypeExt->ProximityDamage.Get(damage);
+ pBulletExt->PassDetonateDamage = pBulletTypeExt->PassDetonateDamage.Get(damage);
+
+ // Record some information of firer
+ if (pFirer)
+ {
+ pBulletExt->FirepowerMult = TechnoExt::GetCurrentFirepowerMultiplier(pFirer);
+
+ // Check trajectory capacity
+ if (pBulletTypeExt->CreateCapacity >= 0)
+ BulletExt::CheckExceededCapacity(pFirer, pBullet->Type, pBulletExt);
+ }
+ else
+ {
+ pBulletExt->NotMainWeapon = true;
+
+ if (pBulletTypeExt->CreateCapacity >= 0)
+ pBulletExt->Status |= TrajectoryStatus::Vanish;
+ }
+
+ // Initialize additional warheads
+ if (pBulletTypeExt->PassDetonate)
+ pBulletExt->PassDetonateTimer.Start(pBulletTypeExt->PassDetonateInitialDelay);
+}
+
+bool BulletExt::ExtData::CheckOnEarlyUpdate()
+{
+ // Update group index for members by themselves
+ if (this->TrajectoryGroup)
+ this->UpdateGroupIndex();
+
+ // In the phase of playing PreImpactAnim
+ if (this->OwnerObject()->SpawnNextAnim)
+ return false;
+
+ // The previous check requires detonation at this time
+ if (this->Status & (TrajectoryStatus::Detonate | TrajectoryStatus::Vanish))
+ return true;
+
+ // Check the remaining existence time
+ if (this->LifeDurationTimer.Completed())
+ return true;
+
+ // After the new target is confirmed, check if the tolerance time has ended
+ if (this->CheckNoTargetLifeTime())
+ return true;
+
+ // Fire weapons or warheads
+ if (this->FireAdditionals())
+ return true;
+
+ // Detonate extra warhead on the obstacle after the pass through check is completed
+ this->DetonateOnObstacle();
+ return false;
+}
+
+void BulletExt::ExtData::CheckOnPreDetonate()
+{
+ const auto pBullet = this->OwnerObject();
+ const auto pBulletTypeExt = this->TypeExtData;
+
+ if (!(this->Status & TrajectoryStatus::Vanish))
+ {
+ if (!pBulletTypeExt->PeacefulVanish.Get(pBulletTypeExt->ProximityImpact || pBulletTypeExt->DisperseCycle))
+ {
+ // Calculate the current damage
+ pBullet->Health = this->GetTrueDamage(pBullet->Health, true);
+ return;
+ }
+
+ this->Status |= TrajectoryStatus::Vanish;
+ }
+
+ // To skip all extra effects, no damage, no anims...
+ pBullet->Health = 0;
+ pBullet->Limbo();
+ pBullet->UnInit();
+}
+
+// Launch additional weapons and warheads
+bool BulletExt::ExtData::FireAdditionals()
+{
+ const auto pType = this->TypeExtData;
+
+ // Detonate the warhead at the current location
+ if (pType->PassDetonate)
+ this->PassWithDetonateAt();
+
+ // Detonate the warhead on the technos passing through
+ if (this->ProximityImpact != 0 && pType->ProximityRadius.Get() > 0)
+ this->PrepareForDetonateAt();
+
+ return false;
+}
+
+// Detonate a extra warhead on the obstacle then detonate bullet itself
+void BulletExt::ExtData::DetonateOnObstacle()
+{
+ const auto pDetonateAt = this->ExtraCheck;
+
+ // Obstacles were detected in the current frame here
+ if (!pDetonateAt)
+ return;
+
+ // Slow down and reset the target
+ this->ExtraCheck = nullptr;
+ const auto pBullet = this->OwnerObject();
+ const double distance = pDetonateAt->GetCoords().DistanceFrom(pBullet->Location);
+
+ // Set the new target so that the snap function can take effect
+ pBullet->SetTarget(pDetonateAt);
+
+ if (const auto pTraj = this->Trajectory.get())
+ {
+ const double speed = pTraj->MovingSpeed;
+
+ // Check whether need to slow down
+ if (speed && distance < speed)
+ pTraj->MultiplyBulletVelocity(distance / speed, true);
+ else
+ this->Status |= TrajectoryStatus::Detonate;
+ }
+
+ // Need to cause additional damage?
+ if (!this->ProximityImpact)
+ return;
+
+ // Detonate extra warhead
+ const auto pFirer = pBullet->Owner;
+ const auto pOwner = pFirer ? pFirer->Owner : BulletExt::ExtMap.Find(pBullet)->FirerHouse;
+ this->ProximityDetonateAt(pOwner, pDetonateAt);
+}
+
+// Tolerance timer inspection
+bool BulletExt::ExtData::CheckNoTargetLifeTime()
+{
+ const auto pBullet = this->OwnerObject();
+ const auto pType = this->TypeExtData;
+
+ // Check should detonate when no target
+ if (!pBullet->Target && !pType->NoTargetLifeTime)
+ return true;
+
+ // Update timer
+ if (pBullet->Target)
+ {
+ this->NoTargetLifeTimer.Stop();
+ }
+ else if (pType->NoTargetLifeTime > 0)
+ {
+ if (this->NoTargetLifeTimer.Completed())
+ return true;
+ else if (!this->NoTargetLifeTimer.IsTicking())
+ this->NoTargetLifeTimer.Start(pType->NoTargetLifeTime);
+ }
+
+ return false;
+}
+
+// Update trajectory capacity group index
+void BulletExt::ExtData::UpdateGroupIndex()
+{
+ const auto pBullet = this->OwnerObject();
+ auto& groupData = (*this->TrajectoryGroup)[pBullet->Type];
+
+ // Should update group index
+ if (groupData.ShouldUpdate)
+ {
+ if (const int size = static_cast(groupData.Bullets.size()))
+ {
+ for (int i = 0; i < size; ++i)
+ {
+ if (groupData.Bullets[i] == pBullet->UniqueID)
+ {
+ this->GroupIndex = i;
+ break;
+ }
+ }
+
+ // If is the last member, reset flag to false
+ if (this->GroupIndex == size - 1)
+ groupData.ShouldUpdate = false;
+ }
+ else
+ {
+ groupData.ShouldUpdate = false;
+ }
+ }
+
+ return;
+}
+
+// Check and set the group
+bool BulletExt::CheckExceededCapacity(TechnoClass* pTechno, BulletTypeClass* pBulletType, BulletExt::ExtData* pBulletExt)
+{
+ const auto pTechnoExt = TechnoExt::ExtMap.Find(pTechno);
+
+ if (!pTechnoExt->TrajectoryGroup)
+ pTechnoExt->TrajectoryGroup = std::make_shared>();
+
+ // Get shared container
+ auto& group = (*pTechnoExt->TrajectoryGroup)[pBulletType].Bullets;
+ const auto size = static_cast(group.size());
+
+ if (!pBulletExt)
+ return size >= BulletTypeExt::ExtMap.Find(pBulletType)->CreateCapacity;
+
+ pBulletExt->TrajectoryGroup = pTechnoExt->TrajectoryGroup;
+
+ // Check trajectory capacity
+ if (size >= pBulletExt->TypeExtData->CreateCapacity)
+ {
+ // Peaceful vanish
+ pBulletExt->Status |= TrajectoryStatus::Vanish;
+ return true;
+ }
+ else
+ {
+ // Increase trajectory count
+ pBulletExt->GroupIndex = size;
+ group.push_back(pBulletExt->OwnerObject()->UniqueID);
+ return false;
+ }
+}
+
void BulletExt::ExtData::InterceptBullet(TechnoClass* pSource, BulletClass* pInterceptor)
{
const auto pThis = this->OwnerObject();
@@ -151,12 +422,13 @@ inline void BulletExt::SimulatedFiringAnim(BulletClass* pBullet, HouseClass* pHo
if (animCounts <= 0)
return;
+ const auto pTraj = BulletExt::ExtMap.Find(pBullet)->Trajectory.get();
+ const auto velocityRadian = pTraj ? Math::atan2(pTraj->MovingVelocity.Y , pTraj->MovingVelocity.X) : Math::atan2(pBullet->Velocity.Y , pBullet->Velocity.X);
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
+ ? (static_cast((velocityRadian / 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;
@@ -446,7 +718,24 @@ void BulletExt::ExtData::Serialize(T& Stm)
.Process(this->DamageNumberOffset)
.Process(this->ParabombFallRate)
- .Process(this->Trajectory) // Keep this shit at last
+ .Process(this->Trajectory)
+ .Process(this->DispersedTrajectory)
+ .Process(this->LifeDurationTimer)
+ .Process(this->NoTargetLifeTimer)
+ .Process(this->FirepowerMult)
+ .Process(this->AttenuationRange)
+ .Process(this->TargetIsInAir)
+ .Process(this->TargetIsTechno)
+ .Process(this->NotMainWeapon)
+ .Process(this->Status)
+ .Process(this->TrajectoryGroup)
+ .Process(this->GroupIndex)
+ .Process(this->PassDetonateDamage)
+ .Process(this->PassDetonateTimer)
+ .Process(this->ProximityImpact)
+ .Process(this->ProximityDamage)
+ .Process(this->ExtraCheck)
+ .Process(this->Casualty)
;
}
@@ -462,6 +751,26 @@ void BulletExt::ExtData::SaveToStream(PhobosStreamWriter& Stm)
this->Serialize(Stm);
}
+bool BulletGroupData::Load(PhobosStreamReader& stm, bool registerForChange)
+{
+ return this->Serialize(stm);
+}
+
+bool BulletGroupData::Save(PhobosStreamWriter& stm) const
+{
+ return const_cast(this)->Serialize(stm);
+}
+
+template
+bool BulletGroupData::Serialize(T& stm)
+{
+ return stm
+ .Process(this->Bullets)
+ .Process(this->Angle)
+ .Process(this->ShouldUpdate)
+ .Success();
+}
+
// =============================
// container
diff --git a/src/Ext/Bullet/Body.h b/src/Ext/Bullet/Body.h
index df7c92b280..8bd210be6b 100644
--- a/src/Ext/Bullet/Body.h
+++ b/src/Ext/Bullet/Body.h
@@ -9,6 +9,22 @@
#include
#include "Trajectories/PhobosTrajectory.h"
+struct BulletGroupData
+{
+ std::vector Bullets {}; // , Capacity
+ double Angle { 0.0 }; // Tracing.StableRotation use this value to update the angle
+ bool ShouldUpdate { true }; // Remind members to update themselves
+
+ BulletGroupData() = default;
+
+ bool Load(PhobosStreamReader& stm, bool registerForChange);
+ bool Save(PhobosStreamWriter& stm) const;
+
+private:
+ template
+ bool Serialize(T& stm);
+};
+
class BulletExt
{
public:
@@ -32,6 +48,23 @@ class BulletExt
int ParabombFallRate;
TrajectoryPointer Trajectory;
+ bool DispersedTrajectory;
+ CDTimerClass LifeDurationTimer;
+ CDTimerClass NoTargetLifeTimer;
+ double FirepowerMult;
+ int AttenuationRange;
+ bool TargetIsInAir;
+ bool TargetIsTechno;
+ bool NotMainWeapon;
+ TrajectoryStatus Status;
+ std::shared_ptr> TrajectoryGroup;
+ int GroupIndex;
+ int PassDetonateDamage;
+ CDTimerClass PassDetonateTimer;
+ int ProximityImpact;
+ int ProximityDamage;
+ TechnoClass* ExtraCheck;
+ std::map Casualty;
ExtData(BulletClass* OwnerObject) : Extension(OwnerObject)
, TypeExtData { nullptr }
@@ -41,13 +74,31 @@ class BulletExt
, InterceptedStatus { InterceptedStatus::None }
, DetonateOnInterception { true }
, LaserTrails {}
- , Trajectory { nullptr }
, SnappedToTarget { false }
, DamageNumberOffset { INT32_MIN }
, ParabombFallRate { 0 }
+
+ , Trajectory { nullptr }
+ , DispersedTrajectory { false }
+ , LifeDurationTimer {}
+ , NoTargetLifeTimer {}
+ , FirepowerMult { 1.0 }
+ , AttenuationRange { 0 }
+ , TargetIsInAir { false }
+ , TargetIsTechno { false }
+ , NotMainWeapon { false }
+ , Status { TrajectoryStatus::None }
+ , TrajectoryGroup {}
+ , GroupIndex { -1 }
+ , PassDetonateDamage { 0 }
+ , PassDetonateTimer {}
+ , ProximityImpact { 0 }
+ , ProximityDamage { 0 }
+ , ExtraCheck { nullptr }
+ , Casualty {}
{ }
- virtual ~ExtData() = default;
+ virtual ~ExtData() override;
virtual void InvalidatePointer(void* ptr, bool bRemoved) override { }
@@ -58,6 +109,23 @@ class BulletExt
void ApplyRadiationToCell(CellStruct cell, int spread, int radLevel);
void InitializeLaserTrails();
+ void InitializeOnUnlimbo();
+ bool CheckOnEarlyUpdate();
+ void CheckOnPreDetonate();
+ bool FireAdditionals();
+ void DetonateOnObstacle();
+ bool CheckNoTargetLifeTime();
+ void UpdateGroupIndex();
+
+ std::vector GetCellsInProximityRadius();
+ bool CheckThroughAndSubjectInCell(CellClass* pCell, HouseClass* pOwner);
+ void CalculateNewDamage();
+ void PassWithDetonateAt();
+ void PrepareForDetonateAt();
+ void ProximityDetonateAt(HouseClass* pOwner, TechnoClass* pTarget);
+ int GetTrueDamage(int damage, bool self);
+ double GetExtraDamageMultiplier();
+
private:
template
void Serialize(T& Stm);
@@ -72,6 +140,8 @@ class BulletExt
static ExtContainer ExtMap;
+ static constexpr double Epsilon = 1e-10;
+
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);
@@ -83,4 +153,49 @@ class BulletExt
static inline void SimulatedFiringElectricBolt(BulletClass* pBullet);
static inline void SimulatedFiringRadBeam(BulletClass* pBullet, HouseClass* pHouse);
static inline void SimulatedFiringParticleSystem(BulletClass* pBullet, HouseClass* pHouse);
+
+ static inline double Get2DDistance(const CoordStruct& coords)
+ {
+ return Point2D { coords.X, coords.Y }.Magnitude();
+ }
+ static inline double Get2DDistance(const CoordStruct& source, const CoordStruct& target)
+ {
+ return Point2D { source.X, source.Y }.DistanceFrom(Point2D { target.X, target.Y });
+ }
+ static inline double Get2DVelocity(const BulletVelocity& velocity)
+ {
+ return Vector2D{ velocity.X, velocity.Y }.Magnitude();
+ }
+ static inline double Get2DOpRadian(const CoordStruct& source, const CoordStruct& target)
+ {
+ return Math::atan2(target.Y - source.Y , target.X - source.X);
+ }
+ static inline BulletVelocity Coord2Vector(const CoordStruct& coords)
+ {
+ return BulletVelocity { static_cast(coords.X), static_cast(coords.Y), static_cast(coords.Z) };
+ }
+ static inline CoordStruct Vector2Coord(const BulletVelocity& velocity)
+ {
+ return CoordStruct { static_cast(velocity.X), static_cast(velocity.Y), static_cast(velocity.Z) };
+ }
+ static inline BulletVelocity HorizontalRotate(const CoordStruct& coords, const double radian)
+ {
+ return BulletVelocity { coords.X * Math::cos(radian) + coords.Y * Math::sin(radian), coords.X * Math::sin(radian) - coords.Y * Math::cos(radian), static_cast(coords.Z) };
+ }
+ static inline bool CheckTechnoIsInvalid(const TechnoClass* const pTechno)
+ {
+ return (!pTechno->IsAlive || !pTechno->IsOnMap || pTechno->InLimbo || pTechno->IsSinking || pTechno->Health <= 0);
+ }
+ static inline void SetNewDamage(int& damage, const double ratio)
+ {
+ if (damage)
+ {
+ if (const auto newDamage = static_cast(damage * ratio))
+ damage = newDamage;
+ else
+ damage = Math::sgn(damage);
+ }
+ }
+ static bool CheckExceededCapacity(TechnoClass* pTechno, BulletTypeClass* pBulletType, BulletExt::ExtData* pBulletExt = nullptr);
+ static std::vector GetCellsInRectangle(const CellStruct bottomStaCell, const CellStruct leftMidCell, const CellStruct rightMidCell, const CellStruct topEndCell);
};
diff --git a/src/Ext/Bullet/Hooks.DetonateLogics.cpp b/src/Ext/Bullet/Hooks.DetonateLogics.cpp
index 11e5ab6ec9..d2e45ead49 100644
--- a/src/Ext/Bullet/Hooks.DetonateLogics.cpp
+++ b/src/Ext/Bullet/Hooks.DetonateLogics.cpp
@@ -17,13 +17,16 @@ DEFINE_HOOK(0x4690D4, BulletClass_Logics_NewChecks, 0x6)
{
enum { SkipShaking = 0x469130, GoToExtras = 0x469AA4 };
- GET(BulletClass*, pBullet, ESI);
+ GET(BulletClass*, pThis, ESI);
GET(WarheadTypeClass*, pWarhead, EAX);
GET_BASE(CoordStruct const* const, pCoords, 0x8);
+ if (BulletExt::ExtMap.Find(pThis)->Status & TrajectoryStatus::Vanish)
+ return GoToExtras;
+
auto const pExt = WarheadTypeExt::ExtMap.Find(pWarhead);
- if (auto const pTarget = abstract_cast(pBullet->Target))
+ if (auto const pTarget = abstract_cast(pThis->Target))
{
// Check if the WH should affect the techno target or skip it
if (!pExt->IsHealthInThreshold(pTarget) || (!pExt->AffectsNeutral && pTarget->Owner->IsNeutral()))
diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp
index e639c7acf8..3c018ec013 100644
--- a/src/Ext/Bullet/Hooks.cpp
+++ b/src/Ext/Bullet/Hooks.cpp
@@ -37,6 +37,8 @@ namespace BulletAITemp
DEFINE_HOOK(0x4666F7, BulletClass_AI, 0x6)
{
+ enum { Detonate = 0x467E53 };
+
GET(BulletClass*, pThis, EBP);
const auto pBulletExt = BulletExt::ExtMap.Find(pThis);
@@ -81,33 +83,43 @@ 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.
- if (!pBulletExt->Trajectory && pBulletExt->LaserTrails.size())
+ // 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 OnEarlyUpdate() to avoid predicting incorrect positions or pass through targets.
+ if (const auto pTraj = pBulletExt->Trajectory.get())
+ {
+ if (pTraj->OnEarlyUpdate() && !pThis->SpawnNextAnim)
+ return Detonate;
+ }
+ else
{
- const CoordStruct location = pThis->GetCoords();
- const BulletVelocity& velocity = pThis->Velocity;
+ if (pBulletExt->CheckOnEarlyUpdate() && !pThis->SpawnNextAnim)
+ return Detonate;
- // 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
+ if (pBulletExt->LaserTrails.size())
{
- (int)(location.X + velocity.X),
- (int)(location.Y + velocity.Y),
- (int)(location.Z + velocity.Z)
- };
+ const CoordStruct location = pThis->GetCoords();
+ const BulletVelocity& velocity = pThis->Velocity;
- 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;
+ // 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
+ const CoordStruct drawnCoords
+ {
+ (int)(location.X + velocity.X),
+ (int)(location.Y + velocity.Y),
+ (int)(location.Z + velocity.Z)
+ };
- pTrail->Update(drawnCoords);
- }
+ 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)
@@ -503,19 +515,6 @@ DEFINE_HOOK(0x44D46E, BuildingClass_Mission_Missile_BeforeMoveTo, 0x8)
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.
diff --git a/src/Ext/Bullet/Trajectories/ActualTrajectories/BombardTrajectory.cpp b/src/Ext/Bullet/Trajectories/ActualTrajectories/BombardTrajectory.cpp
new file mode 100644
index 0000000000..d523ba19e9
--- /dev/null
+++ b/src/Ext/Bullet/Trajectories/ActualTrajectories/BombardTrajectory.cpp
@@ -0,0 +1,493 @@
+#include "BombardTrajectory.h"
+
+#include
+#include
+
+#include
+#include
+
+std::unique_ptr BombardTrajectoryType::CreateInstance(BulletClass* pBullet) const
+{
+ return std::make_unique(this, pBullet);
+}
+
+template
+void BombardTrajectoryType::Serialize(T& Stm)
+{
+ Stm
+ .Process(this->Height)
+ .Process(this->FallPercent)
+ .Process(this->FallPercentShift)
+ .Process(this->FallScatter_Max)
+ .Process(this->FallScatter_Min)
+ .Process(this->FallScatter_Linear)
+ .Process(this->FallSpeed)
+ .Process(this->FreeFallOnTarget)
+ .Process(this->NoLaunch)
+ .Process(this->TurningPointAnims)
+ ;
+}
+
+bool BombardTrajectoryType::Load(PhobosStreamReader& Stm, bool RegisterForChange)
+{
+ this->ActualTrajectoryType::Load(Stm, false);
+ this->Serialize(Stm);
+ return true;
+}
+
+bool BombardTrajectoryType::Save(PhobosStreamWriter& Stm) const
+{
+ this->ActualTrajectoryType::Save(Stm);
+ const_cast(this)->Serialize(Stm);
+ return true;
+}
+
+void BombardTrajectoryType::Read(CCINIClass* const pINI, const char* pSection)
+{
+ this->PhobosTrajectoryType::Read(pINI, pSection);
+ INI_EX exINI(pINI);
+
+ // Actual
+ this->RotateCoord.Read(exINI, pSection, "Trajectory.RotateCoord");
+ this->OffsetCoord.Read(exINI, pSection, "Trajectory.OffsetCoord");
+ this->AxisOfRotation.Read(exINI, pSection, "Trajectory.AxisOfRotation");
+ this->LeadTimeMaximum.Read(exINI, pSection, "Trajectory.LeadTimeMaximum");
+ this->LeadTimeCalculate.Read(exINI, pSection, "Trajectory.LeadTimeCalculate");
+ this->EarlyDetonation.Read(exINI, pSection, "Trajectory.EarlyDetonation");
+ this->DetonationHeight.Read(exINI, pSection, "Trajectory.DetonationHeight");
+ this->DetonationDistance.Read(exINI, pSection, "Trajectory.DetonationDistance");
+ this->TargetSnapDistance.Read(exINI, pSection, "Trajectory.TargetSnapDistance");
+
+ // Bombard
+ this->Height.Read(exINI, pSection, "Trajectory.Bombard.Height");
+ this->Height = Math::max(0.0, this->Height);
+ this->FallPercent.Read(exINI, pSection, "Trajectory.Bombard.FallPercent");
+ this->FallPercentShift.Read(exINI, pSection, "Trajectory.Bombard.FallPercentShift");
+ this->FallScatter_Max.Read(exINI, pSection, "Trajectory.Bombard.FallScatter.Max");
+ this->FallScatter_Min.Read(exINI, pSection, "Trajectory.Bombard.FallScatter.Min");
+ this->FallScatter_Linear.Read(exINI, pSection, "Trajectory.Bombard.FallScatter.Linear");
+ this->FallSpeed.Read(exINI, pSection, "Trajectory.Bombard.FallSpeed");
+ if (this->FallSpeed.isset()) this->FallSpeed = Math::max(0.001, this->FallSpeed);
+ this->FreeFallOnTarget.Read(exINI, pSection, "Trajectory.Bombard.FreeFallOnTarget");
+ this->NoLaunch.Read(exINI, pSection, "Trajectory.Bombard.NoLaunch");
+ this->TurningPointAnims.Read(exINI, pSection, "Trajectory.Bombard.TurningPointAnims");
+}
+
+template
+void BombardTrajectory::Serialize(T& Stm)
+{
+ Stm
+ .Process(this->Type)
+ .Process(this->Height)
+ .Process(this->FallPercent)
+ .Process(this->IsFalling)
+ .Process(this->ToFalling)
+ .Process(this->InitialTargetCoord)
+ .Process(this->RotateRadian)
+ ;
+}
+
+bool BombardTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange)
+{
+ this->ActualTrajectory::Load(Stm, false);
+ this->Serialize(Stm);
+ return true;
+}
+
+bool BombardTrajectory::Save(PhobosStreamWriter& Stm) const
+{
+ this->ActualTrajectory::Save(Stm);
+ const_cast(this)->Serialize(Stm);
+ return true;
+}
+
+void BombardTrajectory::OnUnlimbo()
+{
+ this->ActualTrajectory::OnUnlimbo();
+
+ // Bombard
+ const auto pBullet = this->Bullet;
+
+ // use scaling since RandomRanged only support int
+ this->FallPercent += ScenarioClass::Instance->Random.RandomRanged(0, static_cast(200 * this->Type->FallPercentShift)) / 100.0;
+ this->Height += std::lerp(pBullet->SourceCoords.Z, pBullet->TargetCoords.Z, std::clamp(this->FallPercent, 0.0, 1.0));
+
+ // Record the initial target coordinates without offset
+ this->InitialTargetCoord = pBullet->TargetCoords;
+
+ // Special case: Set the target to the ground
+ if (this->Type->DetonationDistance.Get() <= -BulletExt::Epsilon)
+ {
+ const auto pTarget = pBullet->Target;
+
+ if (pTarget->AbstractFlags & AbstractFlags::Foot)
+ {
+ if (const auto pCell = MapClass::Instance.TryGetCellAt(pTarget->GetCoords()))
+ {
+ pBullet->Target = pCell;
+ pBullet->TargetCoords = pCell->GetCoords();
+ }
+ }
+ }
+
+ this->OpenFire();
+}
+
+bool BombardTrajectory::OnVelocityCheck()
+{
+ return this->BulletVelocityChange() || this->PhobosTrajectory::OnVelocityCheck();
+}
+
+TrajectoryCheckReturnType BombardTrajectory::OnDetonateUpdate(const CoordStruct& position)
+{
+ if (this->WaitStatus != TrajectoryWaitStatus::NowReady)
+ return TrajectoryCheckReturnType::SkipGameCheck;
+ else if (this->PhobosTrajectory::OnDetonateUpdate(position) == TrajectoryCheckReturnType::Detonate)
+ return TrajectoryCheckReturnType::Detonate;
+
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+ this->RemainingDistance -= static_cast(this->MovingSpeed);
+
+ // Check the remaining travel distance of the bullet
+ if (this->IsFalling && this->RemainingDistance < 0)
+ return TrajectoryCheckReturnType::Detonate;
+
+ // Close enough
+ if (pBullet->TargetCoords.DistanceFrom(position) < pType->DetonationDistance.Get())
+ return TrajectoryCheckReturnType::Detonate;
+
+ // Height
+ if (pType->DetonationHeight >= 0 && (pType->EarlyDetonation
+ ? ((position.Z - pBullet->SourceCoords.Z) > pType->DetonationHeight)
+ : (this->IsFalling && (position.Z - pBullet->SourceCoords.Z) < pType->DetonationHeight)))
+ {
+ return TrajectoryCheckReturnType::Detonate;
+ }
+
+ return TrajectoryCheckReturnType::SkipGameCheck;
+}
+
+void BombardTrajectory::OpenFire()
+{
+ const auto pType = this->Type;
+
+ // Wait, or launch immediately?
+ if (!pType->NoLaunch || !pType->LeadTimeCalculate.Get(false) || !abstract_cast(this->Bullet->Target))
+ this->FireTrajectory();
+ else
+ this->WaitStatus = TrajectoryWaitStatus::JustUnlimbo;
+
+ this->PhobosTrajectory::OpenFire();
+}
+
+void BombardTrajectory::FireTrajectory()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+ this->CalculateTargetCoords();
+
+ if (!pType->NoLaunch)
+ {
+ const auto middleLocation = this->CalculateMiddleCoords();
+ this->RemainingDistance += static_cast(middleLocation.DistanceFrom(pBullet->SourceCoords));
+ this->MovingVelocity = BulletExt::Coord2Vector(middleLocation - pBullet->SourceCoords);
+
+ if (this->CalculateBulletVelocity(pType->Speed))
+ BulletExt::ExtMap.Find(pBullet)->Status |= TrajectoryStatus::Detonate;
+ }
+ else
+ {
+ const auto pBulletExt = BulletExt::ExtMap.Find(pBullet);
+ this->ToFalling = true;
+ this->IsFalling = true;
+ auto middleLocation = CoordStruct::Empty;
+
+ if (!pType->FreeFallOnTarget)
+ {
+ middleLocation = this->CalculateMiddleCoords();
+ const double fallSpeed = pType->FallSpeed.Get(pType->Speed);
+ this->RemainingDistance += static_cast(pBullet->TargetCoords.DistanceFrom(middleLocation));
+ this->MovingVelocity = BulletExt::Coord2Vector(pBullet->TargetCoords - middleLocation);
+
+ if (this->CalculateBulletVelocity(fallSpeed))
+ pBulletExt->Status |= TrajectoryStatus::Detonate;
+ }
+ else
+ {
+ middleLocation = CoordStruct { pBullet->TargetCoords.X, pBullet->TargetCoords.Y, static_cast(this->Height) };
+ this->RemainingDistance += (middleLocation.Z - pBullet->TargetCoords.Z);
+ }
+
+ if (pBulletExt->LaserTrails.size())
+ {
+ for (const auto& pTrail : pBulletExt->LaserTrails)
+ pTrail->LastLocation = middleLocation;
+ }
+ this->RefreshBulletLineTrail();
+
+ pBullet->SetLocation(middleLocation);
+ const auto pTechno = pBullet->Owner;
+ const auto pOwner = pTechno ? pTechno->Owner : pBulletExt->FirerHouse;
+ AnimExt::CreateRandomAnim(pType->TurningPointAnims, middleLocation, pTechno, pOwner, true);
+ }
+}
+
+void BombardTrajectory::SetBulletNewTarget(AbstractClass* const pTarget)
+{
+ const auto pBullet = this->Bullet;
+ pBullet->Target = pTarget;
+ pBullet->TargetCoords = pTarget->GetCoords();
+
+ if (this->Type->LeadTimeCalculate.Get(false) && !this->IsFalling)
+ this->LastTargetCoord = pBullet->TargetCoords;
+}
+
+void BombardTrajectory::MultiplyBulletVelocity(const double ratio, const bool shouldDetonate)
+{
+ this->MovingVelocity *= ratio;
+ this->MovingSpeed = this->MovingSpeed * ratio;
+
+ // Only be truly detonated during the descent phase
+ if (shouldDetonate && this->IsFalling)
+ BulletExt::ExtMap.Find(this->Bullet)->Status |= TrajectoryStatus::Detonate;
+}
+
+CoordStruct BombardTrajectory::CalculateMiddleCoords()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+ const int length = ScenarioClass::Instance->Random.RandomRanged(pType->FallScatter_Min.Get(), pType->FallScatter_Max.Get());
+ const double vectorX = (pBullet->TargetCoords.X - pBullet->SourceCoords.X) * this->FallPercent;
+ const double vectorY = (pBullet->TargetCoords.Y - pBullet->SourceCoords.Y) * this->FallPercent;
+ double scatterX = 0.0;
+ double scatterY = 0.0;
+
+ if (!pType->FallScatter_Linear)
+ {
+ const double angel = ScenarioClass::Instance->Random.RandomDouble() * Math::TwoPi;
+ scatterX = length * Math::cos(angel);
+ scatterY = length * Math::sin(angel);
+ }
+ else
+ {
+ const double vectorModule = sqrt(vectorX * vectorX + vectorY * vectorY);
+
+ if (vectorModule <= BulletExt::Epsilon)
+ {
+ scatterX = 0.0;
+ scatterY = 0.0;
+ }
+ else
+ {
+ scatterX = vectorY / vectorModule * length;
+ scatterY = vectorX / vectorModule * length;
+
+ if (ScenarioClass::Instance->Random.RandomRanged(0, 1))
+ scatterX = -scatterX;
+ else
+ scatterY = -scatterY;
+ }
+ }
+
+ return CoordStruct
+ {
+ pBullet->SourceCoords.X + static_cast(vectorX + scatterX),
+ pBullet->SourceCoords.Y + static_cast(vectorY + scatterY),
+ static_cast(this->Height)
+ };
+}
+
+void BombardTrajectory::CalculateTargetCoords()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+ auto& target = pBullet->TargetCoords;
+ const auto& source = pBullet->SourceCoords;
+
+ if (pType->NoLaunch)
+ target += this->CalculateBulletLeadTime();
+
+ // Calculate the orientation of the coordinate system
+ this->RotateRadian = BulletExt::Get2DOpRadian(((target == source && pBullet->Owner) ? pBullet->Owner->GetCoords() : source), target);
+
+ // Add the fixed offset value
+ if (pType->OffsetCoord != CoordStruct::Empty)
+ target += this->GetOnlyStableOffsetCoords(this->RotateRadian);
+
+ // Add random offset value
+ if (pBullet->Type->Inaccurate)
+ target = this->GetInaccurateTargetCoords(target, source.DistanceFrom(target));
+}
+
+CoordStruct BombardTrajectory::CalculateBulletLeadTime()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+
+ if (pType->LeadTimeCalculate.Get(false))
+ {
+ if (const auto pTarget = pBullet->Target)
+ {
+ const auto target = pTarget->GetCoords();
+ const auto& source = pBullet->Location;
+
+ // Solving trigonometric functions
+ if (target != this->LastTargetCoord)
+ {
+ const auto extraOffsetCoord = target - this->LastTargetCoord;
+ const auto targetSourceCoord = source - target;
+ const auto lastSourceCoord = source - this->LastTargetCoord;
+
+ if (pType->FreeFallOnTarget)
+ return extraOffsetCoord * this->GetLeadTime(std::round(sqrt(std::abs(2 * (this->Height - target.Z) / BulletTypeExt::GetAdjustedGravity(pBullet->Type)))));
+
+ if (pType->NoLaunch)
+ return extraOffsetCoord * this->GetLeadTime(std::round((this->Height - target.Z) / pType->FallSpeed.Get(pType->Speed)));
+
+ const double distanceSquared = targetSourceCoord.MagnitudeSquared();
+ const double targetSpeedSquared = extraOffsetCoord.MagnitudeSquared();
+
+ const double crossFactor = lastSourceCoord.CrossProduct(targetSourceCoord).MagnitudeSquared();
+ const double verticalDistanceSquared = crossFactor / targetSpeedSquared;
+
+ const double horizonDistanceSquared = distanceSquared - verticalDistanceSquared;
+ const double horizonDistance = sqrt(horizonDistanceSquared);
+ const double fallSpeed = pType->FallSpeed.Get(pType->Speed);
+
+ // Calculate using vertical distance
+ if (horizonDistance < BulletExt::Epsilon)
+ return extraOffsetCoord * this->GetLeadTime(std::round(sqrt(verticalDistanceSquared) / fallSpeed));
+
+ const double targetSpeed = sqrt(targetSpeedSquared);
+ const double straightSpeedSquared = fallSpeed * fallSpeed;
+ const double baseFactor = straightSpeedSquared - targetSpeedSquared;
+
+ // When the target is moving away, provide an additional frame of correction
+ const int extraTime = distanceSquared >= lastSourceCoord.MagnitudeSquared() ? 2 : 1;
+
+ // Linear equation solving
+ if (std::abs(baseFactor) < BulletExt::Epsilon)
+ return extraOffsetCoord * this->GetLeadTime(static_cast(distanceSquared / (2 * horizonDistance * targetSpeed)) + extraTime);
+
+ const double squareFactor = baseFactor * verticalDistanceSquared + straightSpeedSquared * horizonDistanceSquared;
+
+ // Is there a solution?
+ if (squareFactor > BulletExt::Epsilon)
+ {
+ const double minusFactor = -(horizonDistance * targetSpeed);
+ const double factor = sqrt(squareFactor);
+ const int travelTimeM = static_cast((minusFactor - factor) / baseFactor);
+ const int travelTimeP = static_cast((minusFactor + factor) / baseFactor);
+
+ if (travelTimeM > 0)
+ return extraOffsetCoord * this->GetLeadTime((travelTimeP > 0 ? Math::min(travelTimeM, travelTimeP) : travelTimeM) + extraTime);
+ else if (travelTimeP > 0)
+ return extraOffsetCoord * this->GetLeadTime(travelTimeP + extraTime);
+ }
+ }
+ }
+ }
+
+ return CoordStruct::Empty;
+}
+
+bool BombardTrajectory::BulletVelocityChange()
+{
+ if (!this->IsFalling)
+ {
+ if (this->ToFalling)
+ {
+ this->IsFalling = true;
+ this->RemainingDistance = 1;
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+ const auto pTarget = pBullet->Target;
+ auto middleLocation = CoordStruct::Empty;
+
+ if (!pType->FreeFallOnTarget)
+ {
+ if (pType->LeadTimeCalculate.Get(false) && pTarget)
+ pBullet->TargetCoords += pTarget->GetCoords() - this->InitialTargetCoord + this->CalculateBulletLeadTime();
+
+ middleLocation = pBullet->Location;
+ const double fallSpeed = pType->FallSpeed.Get(pType->Speed);
+ this->MovingVelocity = BulletExt::Coord2Vector(pBullet->TargetCoords - middleLocation);
+
+ if (this->CalculateBulletVelocity(fallSpeed))
+ return true;
+
+ this->RemainingDistance += static_cast(pBullet->TargetCoords.DistanceFrom(middleLocation));
+ }
+ else
+ {
+ if (pType->LeadTimeCalculate.Get(false) && pTarget)
+ pBullet->TargetCoords += pTarget->GetCoords() - this->InitialTargetCoord + this->CalculateBulletLeadTime();
+
+ middleLocation.X = pBullet->TargetCoords.X;
+ middleLocation.Y = pBullet->TargetCoords.Y;
+ middleLocation.Z = pBullet->Location.Z;
+
+ this->MovingSpeed = 0;
+ this->MovingVelocity = BulletVelocity::Empty;
+ this->RemainingDistance += pBullet->Location.Z - MapClass::Instance.GetCellFloorHeight(middleLocation);
+ }
+
+ const auto pBulletExt = BulletExt::ExtMap.Find(pBullet);
+
+ if (pBulletExt->LaserTrails.size())
+ {
+ for (const auto& pTrail : pBulletExt->LaserTrails)
+ pTrail->LastLocation = middleLocation;
+ }
+ this->RefreshBulletLineTrail();
+
+ pBullet->SetLocation(middleLocation);
+ const auto pTechno = pBullet->Owner;
+ const auto pOwner = pTechno ? pTechno->Owner : pBulletExt->FirerHouse;
+ AnimExt::CreateRandomAnim(pType->TurningPointAnims, middleLocation, pTechno, pOwner, true);
+ }
+ else if (this->RemainingDistance < this->MovingSpeed)
+ {
+ this->ToFalling = true;
+ const auto pTarget = this->Bullet->Target;
+
+ if (this->Type->LeadTimeCalculate.Get(false) && pTarget)
+ this->LastTargetCoord = pTarget->GetCoords();
+ }
+ }
+ else if (this->Type->FreeFallOnTarget)
+ {
+ this->MovingSpeed += BulletTypeExt::GetAdjustedGravity(this->Bullet->Type);
+ this->MovingVelocity.Z = -this->MovingSpeed;
+ }
+
+ return false;
+}
+
+void BombardTrajectory::RefreshBulletLineTrail()
+{
+ const auto pBullet = this->Bullet;
+
+ if (const auto pLineTrailer = pBullet->LineTrailer)
+ {
+ pLineTrailer->~LineTrail(); // Should not use GameDelete(pLineTrailer);
+ pBullet->LineTrailer = nullptr;
+ }
+
+ const auto pType = pBullet->Type;
+
+ if (pType->UseLineTrail)
+ {
+ const auto pLineTrailer = GameCreate();
+ pBullet->LineTrailer = pLineTrailer;
+
+ if (RulesClass::Instance->LineTrailColorOverride != ColorStruct { 0, 0, 0 })
+ pLineTrailer->Color = RulesClass::Instance->LineTrailColorOverride;
+ else
+ pLineTrailer->Color = pType->LineTrailColor;
+
+ pLineTrailer->SetDecrement(pType->LineTrailColorDecrement);
+ pLineTrailer->Owner = pBullet;
+ }
+}
diff --git a/src/Ext/Bullet/Trajectories/ActualTrajectories/BombardTrajectory.h b/src/Ext/Bullet/Trajectories/ActualTrajectories/BombardTrajectory.h
new file mode 100644
index 0000000000..83dcf6effb
--- /dev/null
+++ b/src/Ext/Bullet/Trajectories/ActualTrajectories/BombardTrajectory.h
@@ -0,0 +1,88 @@
+#pragma once
+
+#include "../PhobosActualTrajectory.h"
+
+class BombardTrajectoryType final : public ActualTrajectoryType
+{
+public:
+ BombardTrajectoryType() : ActualTrajectoryType()
+ , Height { 0.0 }
+ , FallPercent { 1.0 }
+ , FallPercentShift { 0.0 }
+ , FallScatter_Max { Leptons(0) }
+ , FallScatter_Min { Leptons(0) }
+ , FallScatter_Linear { false }
+ , FallSpeed {}
+ , FreeFallOnTarget { true }
+ , NoLaunch { false }
+ , TurningPointAnims {}
+ {}
+
+ Valueable Height;
+ Valueable FallPercent;
+ Valueable FallPercentShift;
+ Valueable FallScatter_Max;
+ Valueable FallScatter_Min;
+ Valueable FallScatter_Linear;
+ Nullable FallSpeed;
+ Valueable FreeFallOnTarget;
+ Valueable NoLaunch;
+ ValueableVector TurningPointAnims;
+
+ virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override;
+ virtual bool Save(PhobosStreamWriter& Stm) const override;
+ virtual std::unique_ptr CreateInstance(BulletClass* pBullet) const override;
+ virtual void Read(CCINIClass* const pINI, const char* pSection) override;
+ virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Bombard; }
+
+private:
+ template
+ void Serialize(T& Stm);
+};
+
+class BombardTrajectory final : public ActualTrajectory
+{
+public:
+ BombardTrajectory(noinit_t) { }
+ BombardTrajectory(BombardTrajectoryType const* pTrajType, BulletClass* pBullet)
+ : ActualTrajectory(pTrajType, pBullet)
+ , Type { pTrajType }
+ , Height { pTrajType->Height }
+ , FallPercent { pTrajType->FallPercent - pTrajType->FallPercentShift }
+ , IsFalling { false }
+ , ToFalling { false }
+ , InitialTargetCoord {}
+ , RotateRadian { 0 }
+ {}
+
+ const BombardTrajectoryType* Type;
+ double Height;
+ double FallPercent;
+ bool IsFalling;
+ bool ToFalling;
+ CoordStruct InitialTargetCoord;
+ double RotateRadian;
+
+ virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override;
+ virtual bool Save(PhobosStreamWriter& Stm) const override;
+ virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Bombard; }
+ virtual void OnUnlimbo() override;
+ virtual bool OnVelocityCheck() override;
+ virtual TrajectoryCheckReturnType OnDetonateUpdate(const CoordStruct& position) override;
+ virtual const PhobosTrajectoryType* GetType() const override { return this->Type; }
+ virtual void OpenFire() override;
+ virtual void FireTrajectory() override;
+ virtual bool GetCanHitGround() const override { return this->Type->SubjectToGround || this->IsFalling; }
+ virtual void SetBulletNewTarget(AbstractClass* const pTarget) override;
+ virtual void MultiplyBulletVelocity(const double ratio, const bool shouldDetonate) override;
+
+private:
+ CoordStruct CalculateMiddleCoords();
+ void CalculateTargetCoords();
+ CoordStruct CalculateBulletLeadTime();
+ bool BulletVelocityChange();
+ void RefreshBulletLineTrail();
+
+ template
+ void Serialize(T& Stm);
+};
diff --git a/src/Ext/Bullet/Trajectories/ActualTrajectories/ParabolaTrajectory.cpp b/src/Ext/Bullet/Trajectories/ActualTrajectories/ParabolaTrajectory.cpp
new file mode 100644
index 0000000000..c275e243a8
--- /dev/null
+++ b/src/Ext/Bullet/Trajectories/ActualTrajectories/ParabolaTrajectory.cpp
@@ -0,0 +1,1147 @@
+#include "ParabolaTrajectory.h"
+
+#include
+
+#include
+#include
+
+namespace detail
+{
+ template <>
+ inline bool read(ParabolaFireMode& value, INI_EX& parser, const char* pSection, const char* pKey)
+ {
+ if (parser.ReadString(pSection, pKey))
+ {
+ static std::pair FlagNames[] =
+ {
+ {"Speed", ParabolaFireMode::Speed},
+ {"Height", ParabolaFireMode::Height},
+ {"Angle", ParabolaFireMode::Angle},
+ {"SpeedAndHeight", ParabolaFireMode::SpeedAndHeight},
+ {"HeightAndAngle", ParabolaFireMode::HeightAndAngle},
+ {"SpeedAndAngle", ParabolaFireMode::SpeedAndAngle},
+ };
+ for (auto [name, flag] : FlagNames)
+ {
+ if (_strcmpi(parser.value(), name) == 0)
+ {
+ value = flag;
+ return true;
+ }
+ }
+ Debug::INIParseFailed(pSection, pKey, parser.value(), "Expected a new parabola fire mode");
+ }
+
+ return false;
+ }
+}
+
+std::unique_ptr ParabolaTrajectoryType::CreateInstance(BulletClass* pBullet) const
+{
+ return std::make_unique(this, pBullet);
+}
+
+template
+void ParabolaTrajectoryType::Serialize(T& Stm)
+{
+ Stm
+ .Process(this->OpenFireMode)
+ .Process(this->ThrowHeight)
+ .Process(this->LaunchAngle)
+ .Process(this->DetonationAngle)
+ .Process(this->BounceTimes)
+ .Process(this->BounceOnTarget)
+ .Process(this->BounceOnHouses)
+ .Process(this->BounceDetonate)
+ .Process(this->BounceAttenuation)
+ .Process(this->BounceCoefficient)
+ ;
+}
+
+bool ParabolaTrajectoryType::Load(PhobosStreamReader& Stm, bool RegisterForChange)
+{
+ this->ActualTrajectoryType::Load(Stm, false);
+ this->Serialize(Stm);
+ return true;
+}
+
+bool ParabolaTrajectoryType::Save(PhobosStreamWriter& Stm) const
+{
+ this->ActualTrajectoryType::Save(Stm);
+ const_cast(this)->Serialize(Stm);
+ return true;
+}
+
+void ParabolaTrajectoryType::Read(CCINIClass* const pINI, const char* pSection)
+{
+ this->PhobosTrajectoryType::Read(pINI, pSection);
+ INI_EX exINI(pINI);
+
+ // Actual
+ this->RotateCoord.Read(exINI, pSection, "Trajectory.RotateCoord");
+ this->OffsetCoord.Read(exINI, pSection, "Trajectory.OffsetCoord");
+ this->AxisOfRotation.Read(exINI, pSection, "Trajectory.AxisOfRotation");
+ this->LeadTimeMaximum.Read(exINI, pSection, "Trajectory.LeadTimeMaximum");
+ this->LeadTimeCalculate.Read(exINI, pSection, "Trajectory.LeadTimeCalculate");
+ this->EarlyDetonation.Read(exINI, pSection, "Trajectory.EarlyDetonation");
+ this->DetonationHeight.Read(exINI, pSection, "Trajectory.DetonationHeight");
+ this->DetonationDistance.Read(exINI, pSection, "Trajectory.DetonationDistance");
+ this->TargetSnapDistance.Read(exINI, pSection, "Trajectory.TargetSnapDistance");
+
+ // Parabola
+ this->OpenFireMode.Read(exINI, pSection, "Trajectory.Parabola.OpenFireMode");
+ this->ThrowHeight.Read(exINI, pSection, "Trajectory.Parabola.ThrowHeight");
+ this->LaunchAngle.Read(exINI, pSection, "Trajectory.Parabola.LaunchAngle");
+ this->DetonationAngle.Read(exINI, pSection, "Trajectory.Parabola.DetonationAngle");
+ this->BounceTimes.Read(exINI, pSection, "Trajectory.Parabola.BounceTimes");
+ this->BounceOnTarget.Read(exINI, pSection, "Trajectory.Parabola.BounceOnTarget");
+ this->BounceOnHouses.Read(exINI, pSection, "Trajectory.Parabola.BounceOnHouses");
+ this->BounceDetonate.Read(exINI, pSection, "Trajectory.Parabola.BounceDetonate");
+ this->BounceAttenuation.Read(exINI, pSection, "Trajectory.Parabola.BounceAttenuation");
+ this->BounceCoefficient.Read(exINI, pSection, "Trajectory.Parabola.BounceCoefficient");
+}
+
+template
+void ParabolaTrajectory::Serialize(T& Stm)
+{
+ Stm
+ .Process(this->Type)
+ .Process(this->ThrowHeight)
+ .Process(this->BounceTimes)
+ .Process(this->LastVelocity)
+ ;
+}
+
+bool ParabolaTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange)
+{
+ this->ActualTrajectory::Load(Stm, false);
+ this->Serialize(Stm);
+ return true;
+}
+
+bool ParabolaTrajectory::Save(PhobosStreamWriter& Stm) const
+{
+ this->ActualTrajectory::Save(Stm);
+ const_cast(this)->Serialize(Stm);
+ return true;
+}
+
+void ParabolaTrajectory::OnUnlimbo()
+{
+ this->ActualTrajectory::OnUnlimbo();
+
+ // Parabola
+ this->RemainingDistance = INT_MAX;
+ const auto pBullet = this->Bullet;
+
+ // Special case: Set the target to the ground
+ if (this->Type->DetonationDistance.Get() <= -BulletExt::Epsilon)
+ {
+ const auto pTarget = pBullet->Target;
+
+ if (pTarget->AbstractFlags & AbstractFlags::Foot)
+ {
+ if (const auto pCell = MapClass::Instance.TryGetCellAt(pTarget->GetCoords()))
+ {
+ pBullet->Target = pCell;
+ pBullet->TargetCoords = pCell->GetCoords();
+ }
+ }
+ }
+
+ this->OpenFire();
+}
+
+bool ParabolaTrajectory::OnVelocityCheck()
+{
+ const auto pBullet = this->Bullet;
+
+ // Affected by gravity
+ this->MovingVelocity.Z -= BulletTypeExt::GetAdjustedGravity(pBullet->Type);
+ this->MovingSpeed = this->MovingVelocity.Magnitude();
+
+ // Adopting independent logic
+ double ratio = 1.0;
+
+ enum class VelocityCheckType : unsigned char
+ {
+ SkipCheck = 0,
+ CanBounce = 1,
+ Detonate = 2
+ };
+
+ VelocityCheckType velocityCheck = VelocityCheckType::SkipCheck;
+
+ const auto pBulletExt = BulletExt::ExtMap.Find(pBullet);
+ const auto pBulletTypeExt = pBulletExt->TypeExtData;
+ const bool checkThrough = (!pBulletTypeExt->ThroughBuilding || !pBulletTypeExt->ThroughVehicles);
+ const double velocity = BulletExt::Get2DVelocity(this->MovingVelocity);
+
+ // Low speed with checkSubject was already done well
+ if (velocity < Unsorted::LeptonsPerCell)
+ {
+ // Blocked by obstacles?
+ if (checkThrough)
+ {
+ const auto pFirer = pBullet->Owner;
+ const auto pOwner = pFirer ? pFirer->Owner : pBulletExt->FirerHouse;
+
+ // Check for additional obstacles on the ground
+ if (pBulletExt->CheckThroughAndSubjectInCell(MapClass::Instance.GetCellAt(pBullet->Location), pOwner))
+ {
+ if (velocity > PhobosTrajectory::LowSpeedOffset)
+ ratio = (PhobosTrajectory::LowSpeedOffset / velocity);
+
+ velocityCheck = VelocityCheckType::Detonate;
+ }
+ }
+
+ // Check whether about to fall into the ground
+ if (this->BounceTimes > 0 || std::abs(this->MovingVelocity.Z) > Unsorted::CellHeight)
+ {
+ const auto theTargetCoords = pBullet->Location + BulletExt::Vector2Coord(this->MovingVelocity);
+ const int cellHeight = MapClass::Instance.GetCellFloorHeight(theTargetCoords);
+
+ // Check whether the height of the ground is about to exceed the height of the projectile
+ if (cellHeight >= theTargetCoords.Z)
+ {
+ // How much reduction is needed to calculate the velocity vector
+ const double newRatio = std::abs((pBullet->Location.Z - cellHeight) / this->MovingVelocity.Z);
+
+ // Only when the proportion is smaller, it needs to be recorded
+ if (ratio > newRatio)
+ ratio = newRatio;
+
+ velocityCheck = VelocityCheckType::CanBounce;
+ }
+ }
+ }
+ else
+ {
+ // When in high speed, it's necessary to check each cell on the path that the next frame will pass through
+ double locationDistance = 0.0;
+ const auto pBulletType = pBullet->Type;
+
+ // Anyway, at least check the ground
+ const auto& theSourceCoords = pBullet->Location;
+ const auto theTargetCoords = theSourceCoords + BulletExt::Vector2Coord(this->MovingVelocity);
+ const auto pFirer = pBullet->Owner;
+ const auto pOwner = pFirer ? pFirer->Owner : pBulletExt->FirerHouse;
+
+ // No need to use these variables anymore
+ {
+ const auto pSourceCell = MapClass::Instance.GetCellAt(theSourceCoords);
+ const auto pTargetCell = MapClass::Instance.GetCellAt(theTargetCoords);
+ const auto sourceCell = pSourceCell->MapCoords;
+ const auto targetCell = pTargetCell->MapCoords;
+ const bool subjectToWCS = pBulletType->SubjectToWalls || pBulletType->SubjectToCliffs || pBulletTypeExt->SubjectToSolid;
+ const bool checkLevel = !pBulletTypeExt->SubjectToLand.isset() && !pBulletTypeExt->SubjectToWater.isset();
+ const auto cellDist = sourceCell - targetCell;
+ const auto cellPace = CellStruct { static_cast(std::abs(cellDist.X)), static_cast(std::abs(cellDist.Y)) };
+
+ // Take big steps as much as possible to reduce check times, just ensure that each cell is inspected
+ auto largePace = static_cast(Math::max(cellPace.X, cellPace.Y));
+ const auto stepCoord = !largePace ? CoordStruct::Empty : (theTargetCoords - theSourceCoords) * (1.0 / largePace);
+ auto curCoord = theSourceCoords;
+ auto pCurCell = pSourceCell;
+ auto pLastCell = MapClass::Instance.GetCellAt(pBullet->LastMapCoords);
+
+ // Check one by one towards the direction of the next frame's position
+ for (size_t i = 0; i < largePace; ++i)
+ {
+ if ((checkThrough && pBulletExt->CheckThroughAndSubjectInCell(pCurCell, pOwner)) // Blocked by obstacles?
+ || (subjectToWCS && TrajectoryHelper::GetObstacle(pSourceCell, pTargetCell, pLastCell, curCoord, pBulletType, pOwner)) // Impact on the wall/cliff/solid?
+ || (checkLevel ? (pBulletType->Level && pCurCell->IsOnFloor()) // Level or above land/water?
+ : ((pCurCell->LandType == LandType::Water || pCurCell->LandType == LandType::Beach)
+ ? (pBulletTypeExt->SubjectToWater.Get(false) && pBulletTypeExt->SubjectToWater_Detonate)
+ : (pBulletTypeExt->SubjectToLand.Get(false) && pBulletTypeExt->SubjectToLand_Detonate))))
+ {
+ locationDistance = BulletExt::Get2DDistance(curCoord, theSourceCoords);
+ velocityCheck = VelocityCheckType::Detonate;
+ break;
+ }
+ else if (curCoord.Z < MapClass::Instance.GetCellFloorHeight(curCoord)) // Below ground level?
+ {
+ locationDistance = BulletExt::Get2DDistance(curCoord, theSourceCoords);
+ velocityCheck = VelocityCheckType::CanBounce;
+ break;
+ }
+
+ // There are no obstacles, continue to check the next cell
+ curCoord += stepCoord;
+ pLastCell = pCurCell;
+ pCurCell = MapClass::Instance.GetCellAt(curCoord);
+ }
+ }
+
+ // Check whether ignore firestorm wall before searching
+ if (!pBulletType->IgnoresFirestorm)
+ {
+ const auto fireStormCoords = MapClass::Instance.FindFirstFirestorm(theSourceCoords, theTargetCoords, pOwner);
+
+ // Not empty when firestorm wall exists
+ if (fireStormCoords != CoordStruct::Empty)
+ {
+ const double distance = BulletExt::Get2DDistance(fireStormCoords, theSourceCoords);
+
+ // Only record when the ratio is smaller
+ if (velocityCheck == VelocityCheckType::SkipCheck || distance < locationDistance)
+ {
+ locationDistance = distance;
+ velocityCheck = VelocityCheckType::Detonate;
+ }
+ }
+ }
+
+ // Let the distance slightly exceed
+ ratio = (locationDistance + PhobosTrajectory::LowSpeedOffset) / velocity;
+ }
+
+ // No need for change
+ if (velocityCheck == VelocityCheckType::SkipCheck)
+ return false;
+
+ // Detonates itself in the next frame
+ if (velocityCheck == VelocityCheckType::Detonate)
+ {
+ this->MultiplyBulletVelocity(ratio, true);
+ return false;
+ }
+
+ // Bounce in the next frame
+ this->LastVelocity = this->MovingVelocity;
+ this->MultiplyBulletVelocity(ratio, false);
+ return false;
+}
+
+TrajectoryCheckReturnType ParabolaTrajectory::OnDetonateUpdate(const CoordStruct& position)
+{
+ if (this->WaitStatus != TrajectoryWaitStatus::NowReady)
+ return TrajectoryCheckReturnType::SkipGameCheck;
+ else if (this->PhobosTrajectory::OnDetonateUpdate(position) == TrajectoryCheckReturnType::Detonate)
+ return TrajectoryCheckReturnType::Detonate;
+
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+
+ // Close enough
+ if (pBullet->TargetCoords.DistanceFrom(position) < pType->DetonationDistance.Get())
+ return TrajectoryCheckReturnType::Detonate;
+
+ // Height
+ if (pType->DetonationHeight >= 0 && (pType->EarlyDetonation
+ ? ((position.Z - pBullet->SourceCoords.Z) > pType->DetonationHeight)
+ : (this->MovingVelocity.Z < BulletExt::Epsilon && (position.Z - pBullet->SourceCoords.Z) < pType->DetonationHeight)))
+ {
+ return TrajectoryCheckReturnType::Detonate;
+ }
+
+ // Angle
+ if (std::abs(pType->DetonationAngle) < BulletExt::Epsilon)
+ {
+ if (this->MovingVelocity.Z < BulletExt::Epsilon)
+ return TrajectoryCheckReturnType::Detonate;
+ }
+ else if (std::abs(pType->DetonationAngle) < 90.0)
+ {
+ const double velocity = BulletExt::Get2DVelocity(this->MovingVelocity);
+
+ if (velocity > BulletExt::Epsilon)
+ {
+ if ((this->MovingVelocity.Z / velocity) < Math::tan(pType->DetonationAngle * Math::Pi / 180.0))
+ return TrajectoryCheckReturnType::Detonate;
+ }
+ else if (pType->DetonationAngle > BulletExt::Epsilon || this->MovingVelocity.Z < BulletExt::Epsilon)
+ {
+ return TrajectoryCheckReturnType::Detonate;
+ }
+ }
+
+ const auto pCell = MapClass::Instance.TryGetCellAt(position);
+
+ // Bounce
+ if (!pCell || ((BulletExt::ExtMap.Find(pBullet)->Status & TrajectoryStatus::Bounce) && this->CalculateBulletVelocityAfterBounce(pCell, position)))
+ return TrajectoryCheckReturnType::Detonate;
+
+ return TrajectoryCheckReturnType::SkipGameCheck;
+}
+
+void ParabolaTrajectory::OnPreDetonate()
+{
+ const auto pBullet = this->Bullet;
+
+ // If the speed is too fast, it may smash through the floor
+ const int cellHeight = MapClass::Instance.GetCellFloorHeight(pBullet->Location);
+
+ if (pBullet->Location.Z < cellHeight)
+ pBullet->SetLocation(CoordStruct{ pBullet->Location.X, pBullet->Location.Y, cellHeight });
+
+ this->ActualTrajectory::OnPreDetonate();
+}
+
+void ParabolaTrajectory::OpenFire()
+{
+ // Wait, or launch immediately?
+ if (!this->Type->LeadTimeCalculate.Get(false) || !abstract_cast(this->Bullet->Target))
+ this->FireTrajectory();
+ else
+ this->WaitStatus = TrajectoryWaitStatus::JustUnlimbo;
+
+ this->PhobosTrajectory::OpenFire();
+}
+
+void ParabolaTrajectory::FireTrajectory()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+ auto& target = pBullet->TargetCoords;
+ auto& source = pBullet->SourceCoords;
+ const auto pTarget = pBullet->Target;
+
+ if (pTarget)
+ target = pTarget->GetCoords();
+
+ // Calculate the orientation of the coordinate system
+ const double rotateRadian = BulletExt::Get2DOpRadian(((target == source && pBullet->Owner) ? pBullet->Owner->GetCoords() : source), target);
+
+ // Add the fixed offset value
+ if (pType->OffsetCoord != CoordStruct::Empty)
+ target += this->GetOnlyStableOffsetCoords(rotateRadian);
+
+ // Add random offset value
+ if (pBullet->Type->Inaccurate)
+ target = this->GetInaccurateTargetCoords(target, source.DistanceFrom(target));
+
+ // Non positive gravity is not accepted
+ const double gravity = BulletTypeExt::GetAdjustedGravity(pBullet->Type);
+
+ if (gravity <= BulletExt::Epsilon)
+ {
+ BulletExt::ExtMap.Find(pBullet)->Status |= TrajectoryStatus::Detonate;
+ return;
+ }
+
+ // Calculate the firing velocity vector of the bullet
+ if (pType->LeadTimeCalculate.Get(false) && pTarget && pTarget->GetCoords() != this->LastTargetCoord)
+ this->CalculateBulletVelocityLeadTime(source, gravity);
+ else
+ this->CalculateBulletVelocityRightNow(source, gravity);
+
+ this->MovingSpeed = this->MovingVelocity.Magnitude();
+}
+
+void ParabolaTrajectory::MultiplyBulletVelocity(const double ratio, const bool shouldDetonate)
+{
+ if (ratio < 1.0)
+ {
+ this->MovingVelocity *= ratio;
+ this->MovingSpeed = this->MovingSpeed * ratio;
+ }
+
+ // Is it detonating or bouncing?
+ BulletExt::ExtMap.Find(this->Bullet)->Status |= (shouldDetonate || this->BounceTimes <= 0) ? TrajectoryStatus::Detonate : TrajectoryStatus::Bounce;
+}
+
+void ParabolaTrajectory::CalculateBulletVelocityLeadTime(const CoordStruct& source, const double gravity)
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+ const auto target = pBullet->Target->GetCoords();
+ const auto offset = pBullet->TargetCoords - target;
+
+ switch (pType->OpenFireMode)
+ {
+ case ParabolaFireMode::Height: // Fixed max height and aim at the target
+ {
+ // Step 1: Using Newton Iteration Method to determine the time of encounter between the projectile and the target
+ const double meetTime = this->GetLeadTime(this->SearchFixedHeightMeetTime(source, target, offset, gravity));
+
+ // Step 2: Substitute the time into the calculation of the attack coordinates
+ pBullet->TargetCoords += (target - this->LastTargetCoord) * meetTime;
+ const auto destinationCoords = pBullet->TargetCoords - source;
+
+ // Step 3: Check if it is an unsolvable solution
+ if (meetTime <= BulletExt::Epsilon || destinationCoords.Magnitude() <= BulletExt::Epsilon)
+ break;
+
+ // Step 4: Determine the maximum height that the projectile should reach
+ const int sourceHeight = source.Z;
+ const int targetHeight = pBullet->TargetCoords.Z;
+ const int maxHeight = destinationCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight;
+
+ // Step 5: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight)) + gravity / 2;
+
+ // Step 6: Calculate the total time it takes for the projectile to meet the target using the heights of the ascending and descending phases
+ const double time = sqrt(2 * (maxHeight - sourceHeight) / gravity) + sqrt(2 * (maxHeight - targetHeight) / gravity);
+
+ // Step 7: Calculate the horizontal component of the projectile velocity
+ this->MovingVelocity.X = destinationCoords.X / time;
+ this->MovingVelocity.Y = destinationCoords.Y / time;
+ return;
+ }
+ case ParabolaFireMode::Angle: // Fixed fire angle and aim at the target
+ {
+ // Step 1: Read the appropriate fire angle
+ double radian = pType->LaunchAngle * Math::Pi / 180.0;
+ radian = (radian >= Math::HalfPi || radian <= -Math::HalfPi) ? (Math::HalfPi / 3) : radian;
+
+ // Step 2: Using Newton Iteration Method to determine the time of encounter between the projectile and the target
+ const double meetTime = this->GetLeadTime(this->SearchFixedAngleMeetTime(source, target, offset, radian, gravity));
+
+ // Step 3: Substitute the time into the calculation of the attack coordinates
+ pBullet->TargetCoords += (target - this->LastTargetCoord) * meetTime;
+ const auto destinationCoords = pBullet->TargetCoords - source;
+
+ // Step 4: Check if it is an unsolvable solution
+ if (meetTime <= BulletExt::Epsilon || destinationCoords.Magnitude() <= BulletExt::Epsilon)
+ break;
+
+ // Step 5: Recalculate the speed when time is limited
+ if (pType->LeadTimeMaximum > 0)
+ {
+ this->CalculateBulletVelocityRightNow(source, gravity);
+ return;
+ }
+
+ // Step 6: Calculate each horizontal component of the projectile velocity
+ this->MovingVelocity.X = destinationCoords.X / meetTime;
+ this->MovingVelocity.Y = destinationCoords.Y / meetTime;
+
+ // Step 7: Calculate whole horizontal component of the projectile velocity
+ const double horizontalDistance = BulletExt::Get2DDistance(destinationCoords);
+ const double horizontalVelocity = horizontalDistance / meetTime;
+
+ // Step 8: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = horizontalVelocity * Math::tan(radian) + gravity / 2;
+ return;
+ }
+ case ParabolaFireMode::SpeedAndHeight: // Fixed horizontal speed and fixed max height
+ {
+ // Step 1: Calculate the time when the projectile meets the target directly using horizontal velocity
+ const double meetTime = this->GetLeadTime(this->SolveFixedSpeedMeetTime(source, target, offset, pType->Speed));
+
+ // Step 2: Substitute the time into the calculation of the attack coordinates
+ pBullet->TargetCoords += (target - this->LastTargetCoord) * meetTime;
+ const auto destinationCoords = pBullet->TargetCoords - source;
+
+ // Step 3: Check if it is an unsolvable solution
+ if (meetTime <= BulletExt::Epsilon || destinationCoords.Magnitude() <= BulletExt::Epsilon)
+ break;
+
+ // Step 4: Calculate the ratio of horizontal velocity to horizontal distance
+ const double horizontalDistance = BulletExt::Get2DDistance(destinationCoords);
+ const double mult = horizontalDistance > BulletExt::Epsilon ? pType->Speed / horizontalDistance : 1.0;
+
+ // Step 5: Calculate the horizontal component of the projectile velocity
+ this->MovingVelocity.X = destinationCoords.X * mult;
+ this->MovingVelocity.Y = destinationCoords.Y * mult;
+
+ // Step 6: Determine the maximum height that the projectile should reach
+ const int sourceHeight = source.Z;
+ const int targetHeight = sourceHeight + destinationCoords.Z;
+ const int maxHeight = destinationCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight;
+
+ // Step 7: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight)) + gravity / 2;
+ return;
+ }
+ case ParabolaFireMode::HeightAndAngle: // Fixed max height and fixed fire angle
+ {
+ // Step 1: Using Newton Iteration Method to determine the time of encounter between the projectile and the target
+ const double meetTime = this->GetLeadTime(this->SearchFixedHeightMeetTime(source, target, offset, gravity));
+
+ // Step 2: Substitute the time into the calculation of the attack coordinates
+ pBullet->TargetCoords += (target - this->LastTargetCoord) * meetTime;
+ const auto destinationCoords = pBullet->TargetCoords - source;
+
+ // Step 3: Check if it is an unsolvable solution
+ if (meetTime <= BulletExt::Epsilon || destinationCoords.Magnitude() <= BulletExt::Epsilon)
+ break;
+
+ // Step 4: Determine the maximum height that the projectile should reach
+ const int sourceHeight = source.Z;
+ const int targetHeight = sourceHeight + destinationCoords.Z;
+ const int maxHeight = destinationCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight;
+
+ // Step 5: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight)) + gravity / 2;
+
+ // Step 6: Read the appropriate fire angle
+ double radian = pType->LaunchAngle * Math::Pi / 180.0;
+ radian = (radian >= Math::HalfPi || radian <= BulletExt::Epsilon) ? (Math::HalfPi / 3) : radian;
+
+ // Step 7: Calculate the ratio of horizontal velocity to horizontal distance
+ const double horizontalDistance = BulletExt::Get2DDistance(destinationCoords);
+ const double mult = (this->MovingVelocity.Z / Math::tan(radian)) / horizontalDistance;
+
+ // Step 8: Calculate the horizontal component of the projectile velocity
+ this->MovingVelocity.X = destinationCoords.X * mult;
+ this->MovingVelocity.Y = destinationCoords.Y * mult;
+ return;
+ }
+ case ParabolaFireMode::SpeedAndAngle: // Fixed horizontal speed and fixed fire angle
+ {
+ // Step 1: Calculate the time when the projectile meets the target directly using horizontal velocity
+ const double meetTime = this->GetLeadTime(this->SolveFixedSpeedMeetTime(source, target, offset, pType->Speed));
+
+ // Step 2: Substitute the time into the calculation of the attack coordinates
+ pBullet->TargetCoords += (target - this->LastTargetCoord) * meetTime;
+ const auto destinationCoords = pBullet->TargetCoords - source;
+
+ // Step 3: Check if it is an unsolvable solution
+ if (meetTime <= BulletExt::Epsilon || destinationCoords.Magnitude() <= BulletExt::Epsilon)
+ break;
+
+ // Step 4: Calculate the ratio of horizontal velocity to horizontal distance
+ const double horizontalDistance = BulletExt::Get2DDistance(destinationCoords);
+ const double mult = horizontalDistance > BulletExt::Epsilon ? pType->Speed / horizontalDistance : 1.0;
+
+ // Step 5: Calculate each horizontal component of the projectile velocity
+ this->MovingVelocity.X = destinationCoords.X * mult;
+ this->MovingVelocity.Y = destinationCoords.Y * mult;
+
+ // Step 6: Calculate whole horizontal component of the projectile velocity
+ const double horizontalVelocity = horizontalDistance * mult;
+
+ // Step 7: Read the appropriate fire angle
+ double radian = pType->LaunchAngle * Math::Pi / 180.0;
+ radian = (radian >= Math::HalfPi || radian <= -Math::HalfPi) ? (Math::HalfPi / 3) : radian;
+
+ // Step 8: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = horizontalVelocity * Math::tan(radian) + gravity / 2;
+ return;
+ }
+ default: // Fixed horizontal speed and aim at the target
+ {
+ // Step 1: Calculate the time when the projectile meets the target directly using horizontal velocity
+ const double meetTime = this->GetLeadTime(this->SolveFixedSpeedMeetTime(source, target, offset, pType->Speed));
+
+ // Step 2: Substitute the time into the calculation of the attack coordinates
+ pBullet->TargetCoords += (target - this->LastTargetCoord) * meetTime;
+ const auto destinationCoords = pBullet->TargetCoords - source;
+
+ // Step 3: Check if it is an unsolvable solution
+ if (meetTime <= BulletExt::Epsilon || destinationCoords.Magnitude() <= BulletExt::Epsilon)
+ break;
+
+ // Step 4: Calculate the ratio of horizontal velocity to horizontal distance
+ const double horizontalDistance = BulletExt::Get2DDistance(destinationCoords);
+ const double mult = horizontalDistance > BulletExt::Epsilon ? pType->Speed / horizontalDistance : 1.0;
+
+ // Step 5: Calculate the projectile velocity
+ this->MovingVelocity.X = destinationCoords.X * mult;
+ this->MovingVelocity.Y = destinationCoords.Y * mult;
+ this->MovingVelocity.Z = destinationCoords.Z * mult + (gravity * horizontalDistance) / (2 * pType->Speed) + gravity / 2;
+ return;
+ }
+ }
+
+ // Reset target position
+ pBullet->TargetCoords = target + offset;
+
+ // Substitute into the no lead time algorithm
+ this->CalculateBulletVelocityRightNow(source, gravity);
+}
+
+void ParabolaTrajectory::CalculateBulletVelocityRightNow(const CoordStruct& source, const double gravity)
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+
+ // Calculate horizontal distance
+ const auto distanceCoords = pBullet->TargetCoords - source;
+ const double distance = distanceCoords.Magnitude();
+ const double horizontalDistance = BulletExt::Get2DDistance(distanceCoords);
+
+ if (distance <= BulletExt::Epsilon)
+ {
+ BulletExt::ExtMap.Find(pBullet)->Status |= TrajectoryStatus::Detonate;
+ return;
+ }
+
+ switch (pType->OpenFireMode)
+ {
+ case ParabolaFireMode::Height: // Fixed max height and aim at the target
+ {
+ // Step 1: Determine the maximum height that the projectile should reach
+ const int sourceHeight = source.Z;
+ const int targetHeight = pBullet->TargetCoords.Z;
+ const int maxHeight = distanceCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight;
+
+ // Step 2: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight));
+
+ // Step 3: Calculate the total time it takes for the projectile to meet the target using the heights of the ascending and descending phases
+ const double time = sqrt(2 * (maxHeight - sourceHeight) / gravity) + sqrt(2 * (maxHeight - targetHeight) / gravity);
+
+ // Step 4: Calculate the horizontal component of the projectile velocity
+ this->MovingVelocity.X = distanceCoords.X / time;
+ this->MovingVelocity.Y = distanceCoords.Y / time;
+ break;
+ }
+ case ParabolaFireMode::Angle: // Fixed fire angle and aim at the target
+ {
+ // Step 1: Read the appropriate fire angle
+ const double radian = pType->LaunchAngle * Math::Pi / 180.0;
+
+ // Step 2: Using Newton Iteration Method to determine the projectile velocity
+ const double velocity = (radian >= Math::HalfPi || radian <= -Math::HalfPi) ? 100.0 : this->SearchVelocity(horizontalDistance, distanceCoords.Z, radian, gravity);
+
+ // Step 3: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = velocity * Math::sin(radian);
+
+ // Step 4: Calculate the ratio of horizontal velocity to horizontal distance
+ const double mult = velocity * Math::cos(radian) / horizontalDistance;
+
+ // Step 5: Calculate the horizontal component of the projectile velocity
+ this->MovingVelocity.X = distanceCoords.X * mult;
+ this->MovingVelocity.Y = distanceCoords.Y * mult;
+ break;
+ }
+ case ParabolaFireMode::SpeedAndHeight: // Fixed horizontal speed and fixed max height
+ {
+ // Step 1: Determine the maximum height that the projectile should reach
+ const int sourceHeight = source.Z;
+ const int targetHeight = pBullet->TargetCoords.Z;
+ const int maxHeight = distanceCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight;
+
+ // Step 2: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight));
+
+ // Step 3: Calculate the ratio of horizontal velocity to horizontal distance
+ const double mult = horizontalDistance > BulletExt::Epsilon ? pType->Speed / horizontalDistance : 1.0;
+
+ // Step 4: Calculate the horizontal component of the projectile velocity
+ this->MovingVelocity.X = distanceCoords.X * mult;
+ this->MovingVelocity.Y = distanceCoords.Y * mult;
+ break;
+ }
+ case ParabolaFireMode::HeightAndAngle: // Fixed max height and fixed fire angle
+ {
+ // Step 1: Determine the maximum height that the projectile should reach
+ const int sourceHeight = source.Z;
+ const int targetHeight = pBullet->TargetCoords.Z;
+ const int maxHeight = distanceCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight;
+
+ // Step 2: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight));
+
+ // Step 3: Read the appropriate fire angle
+ double radian = pType->LaunchAngle * Math::Pi / 180.0;
+ radian = (radian >= Math::HalfPi || radian <= BulletExt::Epsilon) ? (Math::HalfPi / 3) : radian;
+
+ // Step 4: Calculate the ratio of horizontal velocity to horizontal distance
+ const double mult = (this->MovingVelocity.Z / Math::tan(radian)) / horizontalDistance;
+
+ // Step 5: Calculate the horizontal component of the projectile velocity
+ this->MovingVelocity.X = distanceCoords.X * mult;
+ this->MovingVelocity.Y = distanceCoords.Y * mult;
+ break;
+ }
+ case ParabolaFireMode::SpeedAndAngle: // Fixed horizontal speed and fixed fire angle
+ {
+ // Step 1: Calculate the ratio of horizontal velocity to horizontal distance
+ const double mult = horizontalDistance > BulletExt::Epsilon ? pType->Speed / horizontalDistance : 1.0;
+
+ // Step 2: Calculate the horizontal component of the projectile velocity
+ this->MovingVelocity.X = distanceCoords.X * mult;
+ this->MovingVelocity.Y = distanceCoords.Y * mult;
+
+ // Step 3: Read the appropriate fire angle
+ double radian = pType->LaunchAngle * Math::Pi / 180.0;
+ radian = (radian >= Math::HalfPi || radian <= -Math::HalfPi) ? (Math::HalfPi / 3) : radian;
+
+ // Step 4: Calculate the vertical component of the projectile velocity
+ this->MovingVelocity.Z = pType->Speed * Math::tan(radian);
+ break;
+ }
+ default: // Fixed horizontal speed and aim at the target
+ {
+ // Step 1: Calculate the ratio of horizontal velocity to horizontal distance
+ const double mult = horizontalDistance > BulletExt::Epsilon ? pType->Speed / horizontalDistance : 1.0;
+
+ // Step 2: Calculate the projectile velocity
+ this->MovingVelocity.X = distanceCoords.X * mult;
+ this->MovingVelocity.Y = distanceCoords.Y * mult;
+ this->MovingVelocity.Z = distanceCoords.Z * mult + (gravity * horizontalDistance) / (2 * pType->Speed);
+ break;
+ }
+ }
+
+ // Offset the gravity effect of the first time update
+ this->MovingVelocity.Z += gravity / 2;
+}
+
+double ParabolaTrajectory::SearchVelocity(const double horizontalDistance, int distanceCoordsZ, const double radian, const double gravity)
+{
+ // Estimate initial velocity
+ const double mult = Math::sin(2 * radian);
+ double velocity = std::abs(mult) > BulletExt::Epsilon ? sqrt(horizontalDistance * gravity / mult) : 0.0;
+ velocity += distanceCoordsZ / gravity;
+ velocity = velocity > 8.0 ? velocity : 8.0;
+ const double error = velocity / 16;
+
+ // Newton Iteration Method
+ for (int i = 0; i < ParabolaTrajectory::Attempts; ++i)
+ {
+ // Substitute into the estimate speed
+ const double differential = this->CheckVelocityEquation(horizontalDistance, distanceCoordsZ, velocity, radian, gravity);
+ const double dDifferential = (this->CheckVelocityEquation(horizontalDistance, distanceCoordsZ, (velocity + ParabolaTrajectory::Delta), radian, gravity) - differential) / ParabolaTrajectory::Delta;
+
+ // Check unacceptable divisor
+ if (std::abs(dDifferential) < BulletExt::Epsilon)
+ return velocity;
+
+ // Calculate the speed of the next iteration
+ const double difference = differential / dDifferential;
+ const double velocityNew = velocity - difference;
+
+ // Check tolerable error
+ if (std::abs(difference) < error)
+ return velocityNew;
+
+ // Update the speed
+ velocity = velocityNew;
+ }
+
+ // Unsolvable
+ return 10.0;
+}
+
+double ParabolaTrajectory::CheckVelocityEquation(const double horizontalDistance, int distanceCoordsZ, const double velocity, const double radian, const double gravity)
+{
+ // Calculate each component of the projectile velocity
+ const double horizontalVelocity = velocity * Math::cos(radian);
+ const double verticalVelocity = velocity * Math::sin(radian);
+
+ // Calculate the time of the rising phase
+ const double upTime = verticalVelocity / gravity;
+
+ // Calculate the maximum height that the projectile can reach
+ const double maxHeight = 0.5 * verticalVelocity * upTime;
+
+ // Calculate the time of the descent phase
+ const double downTime = sqrt(2 * (maxHeight - distanceCoordsZ) / gravity);
+
+ // Calculate the total time required for horizontal movement
+ const double wholeTime = horizontalDistance / horizontalVelocity;
+
+ // Calculate the difference between the total vertical motion time and the total horizontal motion time
+ return wholeTime - (upTime + downTime);
+}
+
+double ParabolaTrajectory::SolveFixedSpeedMeetTime(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double horizontalSpeed)
+{
+ // Project all conditions onto a horizontal plane
+ const Point2D targetSpeedCrd { target.X - this->LastTargetCoord.X, target.Y - this->LastTargetCoord.Y };
+ const Point2D destinationCrd { target.X + offset.X - source.X, target.Y + offset.Y - source.Y };
+
+ // Establishing a quadratic equation using time as a variable:
+ // (destinationCrd + targetSpeedCrd * time).Magnitude() = horizontalSpeed * time
+ // Solve this quadratic equation
+ const double targetSpeedSq = targetSpeedCrd.MagnitudeSquared();
+ const double destinationSq = destinationCrd.MagnitudeSquared();
+ const double speedSq = horizontalSpeed * horizontalSpeed;
+ const double divisor = targetSpeedSq - speedSq;
+ const double factor = targetSpeedCrd * destinationCrd;
+ const double cosTheta = factor / (sqrt(targetSpeedSq * destinationSq) + BulletExt::Epsilon);
+
+ // The target speed is too fast
+ if (speedSq < (1.0 + 0.2 * Math::max(0.0, -cosTheta)) * targetSpeedSq)
+ return -1.0;
+
+ // Normal solving
+ const double delta = factor * factor - divisor * destinationSq;
+
+ // Check if there is no solution
+ if (delta < BulletExt::Epsilon)
+ return (delta >= -BulletExt::Epsilon) ? (-factor / divisor) + (factor > 0 ? 1.0 : 0) : -1.0;
+
+ // Quadratic formula
+ const double sqrtDelta = sqrt(delta);
+ const double timeP = (-factor + sqrtDelta) / divisor;
+ const double timeM = (-factor - sqrtDelta) / divisor;
+
+ // When the target is moving away, provide an additional frame of correction
+ if (timeM > BulletExt::Epsilon)
+ return ((timeP > BulletExt::Epsilon) ? Math::min(timeM, timeP) : timeM) + (factor > 0 ? 1.0 : 0);
+ else if (timeP > BulletExt::Epsilon)
+ return timeP + (factor > 0 ? 1.0 : 0);
+
+ // Unsolvable
+ return -1.0;
+}
+
+double ParabolaTrajectory::SearchFixedHeightMeetTime(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double gravity)
+{
+ // Similar to method SearchVelocity, no further elaboration will be provided
+ double meetTime = (this->ThrowHeight << 2) / gravity;
+
+ for (int i = 0; i < ParabolaTrajectory::Attempts; ++i)
+ {
+ const double differential = this->CheckFixedHeightEquation(source, target, offset, meetTime, gravity);
+ const double dDifferential = (this->CheckFixedHeightEquation(source, target, offset, (meetTime + ParabolaTrajectory::Delta), gravity) - differential) / ParabolaTrajectory::Delta;
+
+ if (std::abs(dDifferential) < BulletExt::Epsilon)
+ return meetTime;
+
+ const double difference = differential / dDifferential;
+ const double meetTimeNew = meetTime - difference;
+
+ if (std::abs(difference) < 1.0)
+ return meetTimeNew;
+
+ meetTime = meetTimeNew;
+ }
+
+ return -1.0;
+}
+
+double ParabolaTrajectory::CheckFixedHeightEquation(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double meetTime, const double gravity)
+{
+ // Calculate how high the target will reach during this period of time
+ const int meetHeight = static_cast((target.Z - this->LastTargetCoord.Z) * meetTime) + target.Z + offset.Z;
+
+ // Calculate how high the projectile can fly during this period of time
+ const int maxHeight = meetHeight > source.Z ? this->ThrowHeight + meetHeight : this->ThrowHeight + source.Z;
+
+ // Calculate the difference between these two times
+ return sqrt((maxHeight - source.Z) * 2 / gravity) + sqrt((maxHeight - meetHeight) * 2 / gravity) - meetTime;
+}
+
+double ParabolaTrajectory::SearchFixedAngleMeetTime(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double radian, const double gravity)
+{
+ // Similar to method SearchVelocity, no further elaboration will be provided
+ double meetTime = 512 * Math::sin(radian) / gravity;
+
+ for (int i = 0; i < ParabolaTrajectory::Attempts; ++i)
+ {
+ const double differential = this->CheckFixedAngleEquation(source, target, offset, meetTime, radian, gravity);
+ const double dDifferential = (this->CheckFixedAngleEquation(source, target, offset, (meetTime + ParabolaTrajectory::Delta), radian, gravity) - differential) / ParabolaTrajectory::Delta;
+
+ if (std::abs(dDifferential) < BulletExt::Epsilon)
+ return meetTime;
+
+ const double difference = differential / dDifferential;
+ const double meetTimeNew = meetTime - difference;
+
+ if (std::abs(difference) < 1.0)
+ return meetTimeNew;
+
+ meetTime = meetTimeNew;
+ }
+
+ return -1.0;
+}
+
+double ParabolaTrajectory::CheckFixedAngleEquation(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double meetTime, const double radian, const double gravity)
+{
+ // Using the estimated time to obtain the predicted location of the target
+ const auto distanceCoords = (target - this->LastTargetCoord) * meetTime + target + offset - source;
+
+ // Calculate the horizontal distance between the target and the calculation
+ const double horizontalDistance = BulletExt::Get2DDistance(distanceCoords);
+
+ // Calculate the horizontal velocity
+ const double horizontalVelocity = horizontalDistance / meetTime;
+
+ // Calculate the vertical velocity
+ const double verticalVelocity = horizontalVelocity * Math::tan(radian);
+
+ // Calculate the time of the rising phase
+ const double upTime = verticalVelocity / gravity;
+
+ // Calculate the maximum height that the projectile can reach
+ const double maxHeight = 0.5 * verticalVelocity * upTime;
+
+ // Calculate the time of the descent phase
+ const double downTime = sqrt(2 * (maxHeight - distanceCoords.Z) / gravity);
+
+ // Calculate the difference between the actual flight time of the projectile obtained and the initially estimated time
+ return upTime + downTime - meetTime;
+}
+
+bool ParabolaTrajectory::CalculateBulletVelocityAfterBounce(CellClass* const pCell, const CoordStruct& position)
+{
+ const auto pType = this->Type;
+ const bool alt = pCell->ContainsBridge() && (((pCell->Level + 4) * Unsorted::LevelHeight) <= position.Z);
+
+ // Check can truely bounce on cell
+ if (!EnumFunctions::IsCellEligible(pCell, pType->BounceOnTarget, false, alt))
+ return true;
+
+ // Check can truely bounce on techno
+ const auto pBullet = this->Bullet;
+ const auto pBulletExt = BulletExt::ExtMap.Find(pBullet);
+ const auto pFirer = pBullet->Owner;
+ const auto pOwner = pFirer ? pFirer->Owner : pBulletExt->FirerHouse;
+
+ // Require all technos on the cell to meet the conditions
+ if ((pType->BounceOnTarget & AffectedTarget::AllContents) || pType->BounceOnHouses != AffectedHouse::All)
+ {
+ for (auto pObject = (alt ? pCell->AltObject : pCell->FirstObject); pObject; pObject = pObject->NextObject)
+ {
+ if (const auto pTechno = abstract_cast(pObject))
+ {
+ if (!EnumFunctions::CanTargetHouse(pType->BounceOnHouses, pOwner, pTechno->Owner))
+ return true;
+ else if (!EnumFunctions::IsTechnoEligible(pTechno, pType->BounceOnTarget))
+ return true;
+ }
+ }
+ }
+
+ // Obtain information on which surface to bounce on
+ const auto groundNormalVector = this->GetGroundNormalVector(pCell, position);
+
+ // Bounce only occurs when the velocity is in different directions or the surface is not cliff
+ if (this->LastVelocity * groundNormalVector > 0 && std::abs(groundNormalVector.Z) < BulletExt::Epsilon)
+ {
+ // Restore original velocity
+ this->MovingVelocity = this->LastVelocity;
+ this->MovingSpeed = this->MovingVelocity.Magnitude();
+ return false;
+ }
+
+ // Record bouncing once
+ --this->BounceTimes;
+ pBulletExt->Status &= ~TrajectoryStatus::Bounce;
+
+ // Calculate the velocity vector after bouncing
+ this->MovingVelocity = (this->LastVelocity - groundNormalVector * (this->LastVelocity * groundNormalVector) * 2) * pType->BounceCoefficient;
+ this->MovingSpeed = this->MovingVelocity.Magnitude();
+
+ // Detonate an additional warhead when bouncing?
+ if (pType->BounceDetonate)
+ WarheadTypeExt::DetonateAt(pBullet->WH, position, pFirer, pBullet->Health, pOwner);
+
+ // Calculate the attenuation damage after bouncing
+ BulletExt::SetNewDamage(pBullet->Health, pType->BounceAttenuation);
+ return false;
+}
+
+BulletVelocity ParabolaTrajectory::GetGroundNormalVector(CellClass* const pCell, const CoordStruct& position)
+{
+ if (const auto index = pCell->SlopeIndex)
+ {
+ Vector2D factor { 0.0, 0.0 };
+
+ if (index <= 4)
+ {
+ constexpr double horizontalCommonOffset = Unsorted::LevelHeight / ParabolaTrajectory::SqrtConstexpr(Unsorted::LevelHeight * Unsorted::LevelHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell);
+ constexpr double verticalCommonOffset = Unsorted::LeptonsPerCell / ParabolaTrajectory::SqrtConstexpr(Unsorted::LevelHeight * Unsorted::LevelHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell);
+ factor = Vector2D{ horizontalCommonOffset, verticalCommonOffset };
+ }
+ else if (index <= 12)
+ {
+ constexpr double horizontalTiltOffset = Unsorted::LevelHeight / ParabolaTrajectory::SqrtConstexpr(2 * Unsorted::LevelHeight * Unsorted::LevelHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell);
+ constexpr double verticalTiltOffset = Unsorted::LeptonsPerCell / ParabolaTrajectory::SqrtConstexpr(2 * Unsorted::LevelHeight * Unsorted::LevelHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell);
+ factor = Vector2D{ horizontalTiltOffset, verticalTiltOffset };
+ }
+ else
+ {
+ constexpr double horizontalSteepOffset = Unsorted::CellHeight / ParabolaTrajectory::SqrtConstexpr(2 * Unsorted::CellHeight * Unsorted::CellHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell);
+ constexpr double verticalSteepOffset = Unsorted::LeptonsPerCell / ParabolaTrajectory::SqrtConstexpr(2 * Unsorted::CellHeight * Unsorted::CellHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell);
+ factor = Vector2D{ horizontalSteepOffset, verticalSteepOffset };
+ }
+
+ switch (index)
+ {
+ case 1:
+ return BulletVelocity{ -factor.X, 0.0, factor.Y };
+ case 2:
+ return BulletVelocity{ 0.0, -factor.X, factor.Y };
+ case 3:
+ return BulletVelocity{ factor.X, 0.0, factor.Y };
+ case 4:
+ return BulletVelocity{ 0.0, factor.X, factor.Y };
+ case 5:
+ case 9:
+ case 13:
+ return BulletVelocity{ -factor.X, -factor.X, factor.Y };
+ case 6:
+ case 10:
+ case 14:
+ return BulletVelocity{ factor.X, -factor.X, factor.Y };
+ case 7:
+ case 11:
+ case 15:
+ return BulletVelocity{ factor.X, factor.X, factor.Y };
+ case 8:
+ case 12:
+ case 16:
+ return BulletVelocity{ -factor.X, factor.X, factor.Y };
+ default:
+ return BulletVelocity{ 0.0, 0.0, 1.0 };
+ }
+ }
+
+ constexpr double diagonalLeptonsPerCell = Unsorted::LeptonsPerCell * ParabolaTrajectory::SqrtConstexpr(2);
+ const double horizontalVelocity = BulletExt::Get2DVelocity(this->LastVelocity);
+ const auto velocity = BulletExt::Vector2Coord(horizontalVelocity > diagonalLeptonsPerCell ? this->LastVelocity * (diagonalLeptonsPerCell / horizontalVelocity) : this->LastVelocity);
+ const int cellHeight = pCell->Level * Unsorted::LevelHeight;
+ const int bulletHeight = position.Z;
+ const int lastCellHeight = MapClass::Instance.GetCellFloorHeight(position - velocity);
+
+ // Check if it has hit a cliff (384 -> (4 * Unsorted::LevelHeight - 32(error range)))
+ if (bulletHeight < cellHeight && (cellHeight - lastCellHeight) > 384)
+ {
+ auto cell = pCell->MapCoords;
+ const short reverseSgnX = static_cast(this->LastVelocity.X > 0.0 ? -1 : 1);
+ const short reverseSgnY = static_cast(this->LastVelocity.Y > 0.0 ? -1 : 1);
+
+ enum class CliffType : unsigned char
+ {
+ Type_1_1 = 0,
+ Type_1_2 = 1,
+ Type_2_1 = 2
+ };
+
+ CliffType cliffType = CliffType::Type_1_1;
+
+ // Determine the shape of the cliff using 9 surrounding cells
+ if (this->CheckBulletHitCliff(cell.X + reverseSgnX, cell.Y, bulletHeight, lastCellHeight))
+ {
+ if (!this->CheckBulletHitCliff(cell.X, cell.Y + reverseSgnY, bulletHeight, lastCellHeight))
+ {
+ if (!this->CheckBulletHitCliff(cell.X - reverseSgnX, cell.Y + reverseSgnY, bulletHeight, lastCellHeight))
+ return BulletVelocity{ 0.0, static_cast(reverseSgnY), 0.0 };
+
+ cliffType = CliffType::Type_2_1;
+ }
+ }
+ else
+ {
+ if (this->CheckBulletHitCliff(cell.X + reverseSgnX, cell.Y - reverseSgnY, bulletHeight, lastCellHeight))
+ {
+ if (this->CheckBulletHitCliff(cell.X, cell.Y + reverseSgnY, bulletHeight, lastCellHeight))
+ cliffType = CliffType::Type_1_2;
+ else if (!this->CheckBulletHitCliff(cell.X - reverseSgnX, cell.Y + reverseSgnY, bulletHeight, lastCellHeight))
+ cliffType = CliffType::Type_2_1;
+ }
+ else
+ {
+ if (this->CheckBulletHitCliff(cell.X, cell.Y + reverseSgnY, bulletHeight, lastCellHeight))
+ return BulletVelocity{ static_cast(reverseSgnX), 0.0, 0.0 };
+ else if (this->CheckBulletHitCliff(cell.X - reverseSgnX, cell.Y + reverseSgnY, bulletHeight, lastCellHeight))
+ cliffType = CliffType::Type_1_2;
+ }
+ }
+
+ constexpr double shortRightAngledEdge = 1 / ParabolaTrajectory::SqrtConstexpr(5);
+ constexpr double longRightAngledEdge = 2 / ParabolaTrajectory::SqrtConstexpr(5);
+
+ if (cliffType == CliffType::Type_1_2)
+ return BulletVelocity{ longRightAngledEdge * reverseSgnX, shortRightAngledEdge * reverseSgnY, 0.0 };
+ else if (cliffType == CliffType::Type_2_1)
+ return BulletVelocity{ shortRightAngledEdge * reverseSgnX, longRightAngledEdge * reverseSgnY, 0.0 };
+
+ constexpr double hypotenuse = 1 / ParabolaTrajectory::SqrtConstexpr(2);
+
+ return BulletVelocity{ hypotenuse * reverseSgnX, hypotenuse * reverseSgnY, 0.0 };
+ }
+
+ // Just ordinary ground
+ return BulletVelocity{ 0.0, 0.0, 1.0 };
+}
diff --git a/src/Ext/Bullet/Trajectories/ActualTrajectories/ParabolaTrajectory.h b/src/Ext/Bullet/Trajectories/ActualTrajectories/ParabolaTrajectory.h
new file mode 100644
index 0000000000..e8a82f090c
--- /dev/null
+++ b/src/Ext/Bullet/Trajectories/ActualTrajectories/ParabolaTrajectory.h
@@ -0,0 +1,120 @@
+#pragma once
+
+#include "../PhobosActualTrajectory.h"
+
+enum class ParabolaFireMode : unsigned char
+{
+ Speed = 0,
+ Height = 1,
+ Angle = 2,
+ SpeedAndHeight = 3,
+ HeightAndAngle = 4,
+ SpeedAndAngle = 5
+};
+
+class ParabolaTrajectoryType final : public ActualTrajectoryType
+{
+public:
+ ParabolaTrajectoryType() : ActualTrajectoryType()
+ , OpenFireMode { ParabolaFireMode::Speed }
+ , ThrowHeight { 600 }
+ , LaunchAngle { 30.0 }
+ , DetonationAngle { -90.0 }
+ , BounceTimes { 0 }
+ , BounceOnTarget { AffectedTarget::Land }
+ , BounceOnHouses { AffectedHouse::All }
+ , BounceDetonate { false }
+ , BounceAttenuation { 0.8 }
+ , BounceCoefficient { 0.8 }
+ { }
+
+ Valueable OpenFireMode;
+ Valueable ThrowHeight;
+ Valueable LaunchAngle;
+ Valueable DetonationAngle;
+ Valueable BounceTimes;
+ Valueable BounceOnTarget;
+ Valueable BounceOnHouses;
+ Valueable BounceDetonate;
+ Valueable BounceAttenuation;
+ Valueable BounceCoefficient;
+
+ virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override;
+ virtual bool Save(PhobosStreamWriter& Stm) const override;
+ virtual std::unique_ptr CreateInstance(BulletClass* pBullet) const override;
+ virtual void Read(CCINIClass* const pINI, const char* pSection) override;
+ virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Parabola; }
+
+private:
+ template
+ void Serialize(T& Stm);
+};
+
+class ParabolaTrajectory final : public ActualTrajectory
+{
+public:
+ static constexpr int Attempts = 10;
+ static constexpr double Delta = 1e-5;
+
+ ParabolaTrajectory(noinit_t) { }
+ ParabolaTrajectory(ParabolaTrajectoryType const* pTrajType, BulletClass* pBullet)
+ : ActualTrajectory(pTrajType, pBullet)
+ , Type { pTrajType }
+ , ThrowHeight { pTrajType->ThrowHeight > 0 ? pTrajType->ThrowHeight : 600 }
+ , BounceTimes { pTrajType->BounceTimes }
+ , LastVelocity {}
+ { }
+
+ const ParabolaTrajectoryType* Type;
+ int ThrowHeight;
+ int BounceTimes;
+ BulletVelocity LastVelocity;
+
+ virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override;
+ virtual bool Save(PhobosStreamWriter& Stm) const override;
+ virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Parabola; }
+ virtual void OnUnlimbo() override;
+ virtual bool OnVelocityCheck() override;
+ virtual TrajectoryCheckReturnType OnDetonateUpdate(const CoordStruct& position) override;
+ virtual void OnPreDetonate() override;
+ virtual const PhobosTrajectoryType* GetType() const override { return this->Type; }
+ virtual void OpenFire() override;
+ virtual void FireTrajectory() override;
+ virtual bool GetCanHitGround() const override { return this->BounceTimes <= 0; }
+ virtual void MultiplyBulletVelocity(const double ratio, const bool shouldDetonate) override;
+
+private:
+ void CalculateBulletVelocityRightNow(const CoordStruct& pSourceCoords, const double gravity);
+ void CalculateBulletVelocityLeadTime(const CoordStruct& pSourceCoords, const double gravity);
+ double SearchVelocity(const double horizontalDistance, int distanceCoordsZ, const double radian, const double gravity);
+ double CheckVelocityEquation(const double horizontalDistance, int distanceCoordsZ, const double velocity, const double radian, const double gravity);
+ double SolveFixedSpeedMeetTime(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double horizontalSpeed);
+ double SearchFixedHeightMeetTime(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double gravity);
+ double CheckFixedHeightEquation(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double meetTime, const double gravity);
+ double SearchFixedAngleMeetTime(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double radian, const double gravity);
+ double CheckFixedAngleEquation(const CoordStruct& source, const CoordStruct& target, const CoordStruct& offset, const double meetTime, const double radian, const double gravity);
+ bool CalculateBulletVelocityAfterBounce(CellClass* const pCell, const CoordStruct& position);
+ BulletVelocity GetGroundNormalVector(CellClass* const pCell, const CoordStruct& position);
+
+ static inline bool CheckBulletHitCliff(short X, short Y, int bulletHeight, int lastCellHeight)
+ {
+ if (const auto pCell = MapClass::Instance.TryGetCellAt(CellStruct{ X, Y }))
+ {
+ const auto cellHeight = pCell->Level * Unsorted::LevelHeight;
+
+ // (384 -> (4 * Unsorted::LevelHeight - 32(error range)))
+ if (bulletHeight < cellHeight && (cellHeight - lastCellHeight) > 384)
+ return true;
+ }
+
+ return false;
+ }
+
+ static constexpr double SqrtConstexpr(double x, double curr = 1.0, double prev = 0.0)
+ {
+ return curr == prev ? curr : SqrtConstexpr(x, 0.5 * (curr + x / curr), curr);
+ }
+
+ template
+ void Serialize(T& Stm);
+};
diff --git a/src/Ext/Bullet/Trajectories/ActualTrajectories/StraightTrajectory.cpp b/src/Ext/Bullet/Trajectories/ActualTrajectories/StraightTrajectory.cpp
new file mode 100644
index 0000000000..d6ea82b0b4
--- /dev/null
+++ b/src/Ext/Bullet/Trajectories/ActualTrajectories/StraightTrajectory.cpp
@@ -0,0 +1,368 @@
+#include "StraightTrajectory.h"
+
+#include
+#include
+
+std::unique_ptr StraightTrajectoryType::CreateInstance(BulletClass* pBullet) const
+{
+ return std::make_unique(this, pBullet);
+}
+
+template
+void StraightTrajectoryType::Serialize(T& Stm)
+{
+ Stm
+ .Process(this->PassThrough)
+ .Process(this->ConfineAtHeight)
+ ;
+}
+
+bool StraightTrajectoryType::Load(PhobosStreamReader& Stm, bool RegisterForChange)
+{
+ this->ActualTrajectoryType::Load(Stm, false);
+ this->Serialize(Stm);
+ return true;
+}
+
+bool StraightTrajectoryType::Save(PhobosStreamWriter& Stm) const
+{
+ this->ActualTrajectoryType::Save(Stm);
+ const_cast(this)->Serialize(Stm);
+ return true;
+}
+
+void StraightTrajectoryType::Read(CCINIClass* const pINI, const char* pSection)
+{
+ this->PhobosTrajectoryType::Read(pINI, pSection);
+ INI_EX exINI(pINI);
+
+ // Actual
+ this->RotateCoord.Read(exINI, pSection, "Trajectory.RotateCoord");
+ this->OffsetCoord.Read(exINI, pSection, "Trajectory.OffsetCoord");
+ this->AxisOfRotation.Read(exINI, pSection, "Trajectory.AxisOfRotation");
+ this->LeadTimeMaximum.Read(exINI, pSection, "Trajectory.LeadTimeMaximum");
+ this->LeadTimeCalculate.Read(exINI, pSection, "Trajectory.LeadTimeCalculate");
+ this->DetonationDistance.Read(exINI, pSection, "Trajectory.DetonationDistance");
+ this->TargetSnapDistance.Read(exINI, pSection, "Trajectory.TargetSnapDistance");
+
+ // Straight
+ this->PassThrough.Read(exINI, pSection, "Trajectory.Straight.PassThrough");
+ this->ConfineAtHeight.Read(exINI, pSection, "Trajectory.Straight.ConfineAtHeight");
+}
+
+template
+void StraightTrajectory::Serialize(T& Stm)
+{
+ Stm
+ .Process(this->Type)
+ .Process(this->DetonationDistance)
+ ;
+}
+
+bool StraightTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange)
+{
+ this->ActualTrajectory::Load(Stm, false);
+ this->Serialize(Stm);
+ return true;
+}
+
+bool StraightTrajectory::Save(PhobosStreamWriter& Stm) const
+{
+ this->ActualTrajectory::Save(Stm);
+ const_cast(this)->Serialize(Stm);
+ return true;
+}
+
+void StraightTrajectory::OnUnlimbo()
+{
+ this->ActualTrajectory::OnUnlimbo();
+
+ // Straight
+ const auto pBullet = this->Bullet;
+ const auto pBulletExt = BulletExt::ExtMap.Find(pBullet);
+
+ // Calculate range bonus
+ if (pBulletExt->TypeExtData->ApplyRangeModifiers)
+ {
+ if (const auto pFirer = pBullet->Owner)
+ {
+ if (const auto pWeapon = pBullet->WeaponType)
+ {
+ // Determine the range of the bullet
+ if (this->DetonationDistance >= 0)
+ this->DetonationDistance = Leptons(WeaponTypeExt::GetRangeWithModifiers(pWeapon, pFirer, this->DetonationDistance));
+ else
+ this->DetonationDistance = Leptons(-WeaponTypeExt::GetRangeWithModifiers(pWeapon, pFirer, -this->DetonationDistance));
+ }
+ }
+ }
+
+ this->OpenFire();
+}
+
+bool StraightTrajectory::OnVelocityCheck()
+{
+ const auto pType = this->Type;
+
+ // Hover
+ if (pType->Speed < static_cast(Unsorted::LeptonsPerCell) && pType->ConfineAtHeight > 0 && this->PassAndConfineAtHeight())
+ return true;
+
+ return this->PhobosTrajectory::OnVelocityCheck();
+}
+
+TrajectoryCheckReturnType StraightTrajectory::OnDetonateUpdate(const CoordStruct& position)
+{
+ if (this->WaitStatus != TrajectoryWaitStatus::NowReady)
+ return TrajectoryCheckReturnType::SkipGameCheck;
+ else if (this->PhobosTrajectory::OnDetonateUpdate(position) == TrajectoryCheckReturnType::Detonate)
+ return TrajectoryCheckReturnType::Detonate;
+
+ const auto pType = this->Type;
+ const auto distance = (pType->Speed < static_cast(Unsorted::LeptonsPerCell) && pType->ConfineAtHeight > 0) ? BulletExt::Get2DVelocity(this->MovingVelocity) : this->MovingSpeed;
+ this->RemainingDistance -= static_cast(distance);
+
+ // Check the remaining travel distance of the bullet
+ if (this->RemainingDistance < 0)
+ return TrajectoryCheckReturnType::Detonate;
+
+ const auto pBullet = this->Bullet;
+
+ // Close enough
+ if (!pType->PassThrough && pBullet->TargetCoords.DistanceFrom(position) < pType->DetonationDistance.Get())
+ return TrajectoryCheckReturnType::Detonate;
+
+ return TrajectoryCheckReturnType::SkipGameCheck;
+}
+
+void StraightTrajectory::OnPreDetonate()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+
+ // Whether to detonate at ground level?
+ if (BulletTypeExt::ExtMap.Find(pBullet->Type)->PassDetonateLocal)
+ pBullet->SetLocation(CoordStruct { pBullet->Location.X, pBullet->Location.Y, MapClass::Instance.GetCellFloorHeight(pBullet->Location) });
+
+ if (!pType->PassThrough)
+ this->ActualTrajectory::OnPreDetonate();
+ else
+ this->PhobosTrajectory::OnPreDetonate();
+}
+
+void StraightTrajectory::OpenFire()
+{
+ // Wait, or launch immediately?
+ if (!this->Type->LeadTimeCalculate.Get(false) || !abstract_cast(this->Bullet->Target))
+ this->FireTrajectory();
+ else
+ this->WaitStatus = TrajectoryWaitStatus::JustUnlimbo;
+
+ this->PhobosTrajectory::OpenFire();
+}
+
+void StraightTrajectory::FireTrajectory()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+ const auto& source = pBullet->SourceCoords;
+ auto& target = pBullet->TargetCoords;
+ target += this->CalculateBulletLeadTime();
+
+ // Calculate the orientation of the coordinate system
+ const auto rotateRadian = BulletExt::Get2DOpRadian(((target == source && pBullet->Owner) ? pBullet->Owner->GetCoords() : source), target);
+
+ // Add the fixed offset value
+ if (pType->OffsetCoord != CoordStruct::Empty)
+ target += this->GetOnlyStableOffsetCoords(rotateRadian);
+
+ // Add random offset value
+ if (pBullet->Type->Inaccurate)
+ target = this->GetInaccurateTargetCoords(target, source.DistanceFrom(target));
+
+ // Determine the distance that the bullet can travel
+ if (!pType->PassThrough)
+ this->RemainingDistance += static_cast(source.DistanceFrom(target));
+ else if (this->DetonationDistance > 0)
+ this->RemainingDistance += static_cast(this->DetonationDistance);
+ else if (this->DetonationDistance < 0)
+ this->RemainingDistance += static_cast(source.DistanceFrom(target) - this->DetonationDistance);
+ else
+ this->RemainingDistance = INT_MAX;
+
+ // Determine the firing velocity vector of the bullet
+ pBullet->TargetCoords = target;
+ this->MovingVelocity.X = static_cast(target.X - source.X);
+ this->MovingVelocity.Y = static_cast(target.Y - source.Y);
+ this->MovingVelocity.Z = (pType->Speed < static_cast(Unsorted::LeptonsPerCell) && pType->ConfineAtHeight > 0 && BulletTypeExt::ExtMap.Find(pBullet->Type)->PassDetonateLocal) ? 0 : static_cast(this->GetVelocityZ());
+
+ // Substitute the speed to calculate velocity
+ if (this->CalculateBulletVelocity(pType->Speed))
+ BulletExt::ExtMap.Find(pBullet)->Status |= TrajectoryStatus::Detonate;
+}
+
+CoordStruct StraightTrajectory::CalculateBulletLeadTime()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+
+ if (pType->LeadTimeCalculate.Get(false))
+ {
+ if (const auto pTarget = pBullet->Target)
+ {
+ const auto target = pTarget->GetCoords();
+ const auto source = pBullet->Location;
+
+ // Solving trigonometric functions
+ if (target != this->LastTargetCoord)
+ {
+ const auto extraOffsetCoord = target - this->LastTargetCoord;
+ const auto targetSourceCoord = source - target;
+ const auto lastSourceCoord = source - this->LastTargetCoord;
+
+ const double distanceSquared = targetSourceCoord.MagnitudeSquared();
+ const double targetSpeedSquared = extraOffsetCoord.MagnitudeSquared();
+
+ const double crossFactor = lastSourceCoord.CrossProduct(targetSourceCoord).MagnitudeSquared();
+ const double verticalDistanceSquared = crossFactor / targetSpeedSquared;
+
+ const double horizonDistanceSquared = distanceSquared - verticalDistanceSquared;
+ const double horizonDistance = sqrt(horizonDistanceSquared);
+
+ // Calculate using vertical distance
+ if (horizonDistance < BulletExt::Epsilon)
+ return extraOffsetCoord * this->GetLeadTime(std::round(sqrt(verticalDistanceSquared) / pType->Speed));
+
+ const double targetSpeed = sqrt(targetSpeedSquared);
+ const double straightSpeedSquared = pType->Speed * pType->Speed;
+ const double baseFactor = straightSpeedSquared - targetSpeedSquared;
+
+ // When the target is moving away, provide an additional frame of correction
+ const int extraTime = distanceSquared >= lastSourceCoord.MagnitudeSquared() ? 2 : 1;
+
+ // Linear equation solving
+ if (std::abs(baseFactor) < BulletExt::Epsilon)
+ return extraOffsetCoord * this->GetLeadTime(static_cast(distanceSquared / (2 * horizonDistance * targetSpeed)) + extraTime);
+
+ const double squareFactor = baseFactor * verticalDistanceSquared + straightSpeedSquared * horizonDistanceSquared;
+
+ // Is there a solution?
+ if (squareFactor > BulletExt::Epsilon)
+ {
+ const double minusFactor = -(horizonDistance * targetSpeed);
+ const double factor = sqrt(squareFactor);
+ const int travelTimeM = static_cast((minusFactor - factor) / baseFactor);
+ const int travelTimeP = static_cast((minusFactor + factor) / baseFactor);
+
+ if (travelTimeM > 0)
+ return extraOffsetCoord * this->GetLeadTime((travelTimeP > 0 ? Math::min(travelTimeM, travelTimeP) : travelTimeM) + extraTime);
+ else if (travelTimeP > 0)
+ return extraOffsetCoord * this->GetLeadTime(travelTimeP + extraTime);
+ }
+ }
+ }
+ }
+
+ return CoordStruct::Empty;
+}
+
+int StraightTrajectory::GetVelocityZ()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+ int sourceCellZ = pBullet->SourceCoords.Z;
+ int targetCellZ = pBullet->TargetCoords.Z;
+ int bulletVelocityZ = static_cast(targetCellZ - sourceCellZ);
+
+ // Subtract directly if no need to pass through the target
+ if (!pType->PassThrough)
+ return bulletVelocityZ;
+
+ if (const auto pTechno = pBullet->Owner)
+ {
+ const auto pCell = pTechno->GetCell();
+ sourceCellZ = pCell->Level * Unsorted::LevelHeight;
+
+ if (pCell->ContainsBridge() && pTechno->OnBridge)
+ sourceCellZ += CellClass::BridgeHeight;
+ }
+
+ if (const auto pTarget = abstract_cast(pBullet->Target))
+ {
+ const auto pCell = pTarget->GetCell();
+ targetCellZ = pCell->Level * Unsorted::LevelHeight;
+
+ if (pCell->ContainsBridge() && pTarget->OnBridge)
+ targetCellZ += CellClass::BridgeHeight;
+ }
+
+ // If both are at the same height, use the DetonationDistance to calculate which position behind the target needs to be aimed (32 -> error range)
+ if (sourceCellZ == targetCellZ || std::abs(bulletVelocityZ) <= 32)
+ {
+ // Infinite distance, horizontal emission
+ if (!this->DetonationDistance)
+ return 0;
+
+ const double distanceOfTwo = BulletExt::Get2DDistance(pBullet->SourceCoords, pBullet->TargetCoords);
+ const double theDistance = (this->DetonationDistance < 0) ? (distanceOfTwo - this->DetonationDistance) : this->DetonationDistance;
+
+ // Calculate the ratio for subsequent speed calculation
+ if (std::abs(theDistance) < BulletExt::Epsilon)
+ return 0;
+
+ bulletVelocityZ = static_cast(bulletVelocityZ * (distanceOfTwo / theDistance));
+ }
+
+ return bulletVelocityZ;
+}
+
+bool StraightTrajectory::PassAndConfineAtHeight()
+{
+ const auto pBullet = this->Bullet;
+ const auto pType = this->Type;
+
+ // To prevent twitching and floating up and down, it is necessary to maintain a fixed distance when predicting the position
+ const double horizontalVelocity = BulletExt::Get2DVelocity(this->MovingVelocity);
+
+ if (horizontalVelocity <= BulletExt::Epsilon)
+ return false;
+
+ const double ratio = pType->Speed / horizontalVelocity;
+ auto velocityCoords = BulletExt::Vector2Coord(this->MovingVelocity);
+ velocityCoords.X = static_cast(velocityCoords.X * ratio);
+ velocityCoords.Y = static_cast(velocityCoords.Y * ratio);
+ const auto futureCoords = pBullet->Location + velocityCoords;
+ int checkDifference = MapClass::Instance.GetCellFloorHeight(futureCoords) - futureCoords.Z;
+
+ // Bridges require special treatment
+ if (MapClass::Instance.GetCellAt(futureCoords)->ContainsBridge())
+ {
+ const int differenceOnBridge = checkDifference + CellClass::BridgeHeight;
+
+ if (std::abs(differenceOnBridge) < std::abs(checkDifference))
+ checkDifference = differenceOnBridge;
+ }
+
+ // The height does not exceed the cliff, or the cliff can be ignored? (384 -> (4 * Unsorted::LevelHeight - 32(error range)))
+ if (std::abs(checkDifference) >= 384 && pBullet->Type->SubjectToCliffs)
+ return true;
+
+ this->MovingVelocity.Z += static_cast(checkDifference + pType->ConfineAtHeight);
+
+ if (BulletTypeExt::ExtMap.Find(pBullet->Type)->PassDetonateLocal)
+ {
+ // In this case, the vertical speed will not be limited, and the horizontal speed will not be affected
+ this->MovingSpeed = this->MovingVelocity.Magnitude();
+ }
+ else
+ {
+ // The maximum climbing ratio is limited to 8:1
+ const double maxZ = horizontalVelocity * 8;
+ this->MovingVelocity.Z = Math::clamp(this->MovingVelocity.Z, -maxZ, maxZ);
+
+ if (this->CalculateBulletVelocity(pType->Speed))
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/Ext/Bullet/Trajectories/ActualTrajectories/StraightTrajectory.h b/src/Ext/Bullet/Trajectories/ActualTrajectories/StraightTrajectory.h
new file mode 100644
index 0000000000..d54a10b742
--- /dev/null
+++ b/src/Ext/Bullet/Trajectories/ActualTrajectories/StraightTrajectory.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "../PhobosActualTrajectory.h"
+
+class StraightTrajectoryType final : public ActualTrajectoryType
+{
+public:
+ StraightTrajectoryType() : ActualTrajectoryType()
+ , PassThrough { false }
+ , ConfineAtHeight { 0 }
+ { }
+
+ Valueable PassThrough;
+ Valueable ConfineAtHeight;
+
+ virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override;
+ virtual bool Save(PhobosStreamWriter& Stm) const override;
+ virtual std::unique_ptr CreateInstance(BulletClass* pBullet) const override;
+ virtual void Read(CCINIClass* const pINI, const char* pSection) override;
+ virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Straight; }
+
+private:
+ template
+ void Serialize(T& Stm);
+};
+
+class StraightTrajectory final : public ActualTrajectory
+{
+public:
+ StraightTrajectory(noinit_t) { }
+ StraightTrajectory(StraightTrajectoryType const* pTrajType, BulletClass* pBullet)
+ : ActualTrajectory(pTrajType, pBullet)
+ , Type { pTrajType }
+ , DetonationDistance { pTrajType->DetonationDistance }
+ { }
+
+ const StraightTrajectoryType* Type;
+ Leptons DetonationDistance;
+
+ virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override;
+ virtual bool Save(PhobosStreamWriter& Stm) const override;
+ virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Straight; }
+ virtual void OnUnlimbo() override;
+ virtual bool OnVelocityCheck() override;
+ virtual TrajectoryCheckReturnType OnDetonateUpdate(const CoordStruct& position) override;
+ virtual void OnPreDetonate() override;
+ virtual const PhobosTrajectoryType* GetType() const override { return this->Type; }
+ virtual void OpenFire() override;
+ virtual void FireTrajectory() override;
+ virtual bool GetCanHitGround() const override { return this->Type->SubjectToGround; }
+
+private:
+ CoordStruct CalculateBulletLeadTime();
+ int GetVelocityZ();
+ bool PassAndConfineAtHeight();
+
+ template
+ void Serialize(T& Stm);
+};
diff --git a/src/Ext/Bullet/Trajectories/BombardTrajectory.cpp b/src/Ext/Bullet/Trajectories/BombardTrajectory.cpp
deleted file mode 100644
index 27f979d4f1..0000000000
--- a/src/Ext/Bullet/Trajectories/BombardTrajectory.cpp
+++ /dev/null
@@ -1,615 +0,0 @@
-#include "BombardTrajectory.h"
-#include "Memory.h"
-
-#include
-#include
-#include
-#include
-
-std::unique_ptr BombardTrajectoryType::CreateInstance() const
-{
- return std::make_unique(this);
-}
-
-template
-void BombardTrajectoryType::Serialize(T& Stm)
-{
- Stm
- .Process(this->Height)
- .Process(this->FallPercent)
- .Process(this->FallPercentShift)
- .Process(this->FallScatter_Max)
- .Process(this->FallScatter_Min)
- .Process(this->FallScatter_Linear)
- .Process(this->FallSpeed)
- .Process(this->DetonationDistance)
- .Process(this->DetonationHeight)
- .Process(this->EarlyDetonation)
- .Process(this->TargetSnapDistance)
- .Process(this->FreeFallOnTarget)
- .Process(this->LeadTimeCalculate)
- .Process(this->NoLaunch)
- .Process(this->TurningPointAnims)
- .Process(this->OffsetCoord)
- .Process(this->RotateCoord)
- .Process(this->MirrorCoord)
- .Process(this->UseDisperseBurst)
- .Process(this->AxisOfRotation)
- .Process(this->SubjectToGround)
- ;
-}
-
-bool BombardTrajectoryType::Load(PhobosStreamReader& Stm, bool RegisterForChange)
-{
- this->PhobosTrajectoryType::Load(Stm, false);
- this->Serialize(Stm);
- return true;
-}
-
-bool BombardTrajectoryType::Save(PhobosStreamWriter& Stm) const
-{
- this->PhobosTrajectoryType::Save(Stm);
- const_cast(this)->Serialize(Stm);
- return true;
-}
-
-void BombardTrajectoryType::Read(CCINIClass* const pINI, const char* pSection)
-{
- INI_EX exINI(pINI);
-
- this->Height.Read(exINI, pSection, "Trajectory.Bombard.Height");
- this->Height = Math::max(0.0, this->Height);
- this->FallPercent.Read(exINI, pSection, "Trajectory.Bombard.FallPercent");
- this->FallPercentShift.Read(exINI, pSection, "Trajectory.Bombard.FallPercentShift");
- this->FallScatter_Max.Read(exINI, pSection, "Trajectory.Bombard.FallScatter.Max");
- this->FallScatter_Min.Read(exINI, pSection, "Trajectory.Bombard.FallScatter.Min");
- this->FallScatter_Linear.Read(exINI, pSection, "Trajectory.Bombard.FallScatter.Linear");
- this->FallSpeed.Read(exINI, pSection, "Trajectory.Bombard.FallSpeed");
- this->FallSpeed = std::abs(this->FallSpeed.Get()) < 1e-10 ? this->Trajectory_Speed.Get() : this->FallSpeed.Get();
- this->DetonationDistance.Read(exINI, pSection, "Trajectory.Bombard.DetonationDistance");
- this->DetonationHeight.Read(exINI, pSection, "Trajectory.Bombard.DetonationHeight");
- this->EarlyDetonation.Read(exINI, pSection, "Trajectory.Bombard.EarlyDetonation");
- this->TargetSnapDistance.Read(exINI, pSection, "Trajectory.Bombard.TargetSnapDistance");
- this->FreeFallOnTarget.Read(exINI, pSection, "Trajectory.Bombard.FreeFallOnTarget");
- this->LeadTimeCalculate.Read(exINI, pSection, "Trajectory.Bombard.LeadTimeCalculate");
- this->NoLaunch.Read(exINI, pSection, "Trajectory.Bombard.NoLaunch");
- this->TurningPointAnims.Read(exINI, pSection, "Trajectory.Bombard.TurningPointAnims");
- this->OffsetCoord.Read(exINI, pSection, "Trajectory.Bombard.OffsetCoord");
- this->RotateCoord.Read(exINI, pSection, "Trajectory.Bombard.RotateCoord");
- this->MirrorCoord.Read(exINI, pSection, "Trajectory.Bombard.MirrorCoord");
- this->UseDisperseBurst.Read(exINI, pSection, "Trajectory.Bombard.UseDisperseBurst");
- this->AxisOfRotation.Read(exINI, pSection, "Trajectory.Bombard.AxisOfRotation");
- this->SubjectToGround.Read(exINI, pSection, "Trajectory.Bombard.SubjectToGround");
-}
-
-template
-void BombardTrajectory::Serialize(T& Stm)
-{
- Stm
- .Process(this->Type)
- .Process(this->Height)
- .Process(this->FallPercent)
- .Process(this->OffsetCoord)
- .Process(this->UseDisperseBurst)
- .Process(this->IsFalling)
- .Process(this->ToFalling)
- .Process(this->RemainingDistance)
- .Process(this->LastTargetCoord)
- .Process(this->InitialTargetCoord)
- .Process(this->CountOfBurst)
- .Process(this->CurrentBurst)
- .Process(this->RotateAngle)
- .Process(this->WaitOneFrame)
- ;
-}
-
-bool BombardTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange)
-{
- this->Serialize(Stm);
- return true;
-}
-
-bool BombardTrajectory::Save(PhobosStreamWriter& Stm) const
-{
- const_cast(this)->Serialize(Stm);
- return true;
-}
-
-void BombardTrajectory::OnUnlimbo(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity)
-{
- const auto pType = this->Type;
- this->Height += pBullet->TargetCoords.Z;
- // use scaling since RandomRanged only support int
- this->FallPercent += ScenarioClass::Instance->Random.RandomRanged(0, static_cast(200 * pType->FallPercentShift)) / 100.0;
-
- // Record the initial target coordinates without offset
- this->InitialTargetCoord = pBullet->TargetCoords;
- this->LastTargetCoord = pBullet->TargetCoords;
- pBullet->Velocity = BulletVelocity::Empty;
-
- // Record some information
- if (const auto pWeapon = pBullet->WeaponType)
- this->CountOfBurst = pWeapon->Burst;
-
- if (const auto pOwner = pBullet->Owner)
- {
- this->CurrentBurst = pOwner->CurrentBurstIndex;
-
- if (pType->MirrorCoord && pOwner->CurrentBurstIndex % 2 == 1)
- this->OffsetCoord.Y = -(this->OffsetCoord.Y);
- }
-
- // Wait, or launch immediately?
- if (!pType->NoLaunch || !pType->LeadTimeCalculate || !abstract_cast(pBullet->Target))
- this->PrepareForOpenFire(pBullet);
- else
- this->WaitOneFrame = 2;
-}
-
-bool BombardTrajectory::OnAI(BulletClass* pBullet)
-{
- if (this->WaitOneFrame && this->BulletPrepareCheck(pBullet))
- return false;
-
- if (this->BulletDetonatePreCheck(pBullet))
- return true;
-
- this->BulletVelocityChange(pBullet);
-
- // Extra check for trajectory falling
- if (this->IsFalling && !this->Type->FreeFallOnTarget && this->BulletDetonateRemainCheck(pBullet))
- return true;
-
- return false;
-}
-
-void BombardTrajectory::OnAIPreDetonate(BulletClass* pBullet)
-{
- const auto pType = this->Type;
- const auto pTarget = abstract_cast(pBullet->Target);
- const auto pCoords = pTarget ? pTarget->GetCoords() : pBullet->Data.Location;
-
- if (pCoords.DistanceFrom(pBullet->Location) <= pType->TargetSnapDistance.Get())
- {
- const auto pExt = BulletExt::ExtMap.Find(pBullet);
- pExt->SnappedToTarget = true;
- pBullet->SetLocation(pCoords);
- }
-}
-
-void BombardTrajectory::OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition)
-{
- pSpeed->Z += BulletTypeExt::GetAdjustedGravity(pBullet->Type); // We don't want to take the gravity into account
-}
-
-TrajectoryCheckReturnType BombardTrajectory::OnAITargetCoordCheck(BulletClass* pBullet)
-{
- return TrajectoryCheckReturnType::SkipGameCheck; // Bypass game checks entirely.
-}
-
-TrajectoryCheckReturnType BombardTrajectory::OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno)
-{
- return TrajectoryCheckReturnType::SkipGameCheck; // Bypass game checks entirely.
-}
-
-void BombardTrajectory::PrepareForOpenFire(BulletClass* pBullet)
-{
- const auto pType = this->Type;
- this->CalculateTargetCoords(pBullet);
-
- if (!pType->NoLaunch)
- {
- const auto middleLocation = this->CalculateMiddleCoords(pBullet);
-
- pBullet->Velocity.X = static_cast(middleLocation.X - pBullet->SourceCoords.X);
- pBullet->Velocity.Y = static_cast(middleLocation.Y - pBullet->SourceCoords.Y);
- pBullet->Velocity.Z = static_cast(middleLocation.Z - pBullet->SourceCoords.Z);
- pBullet->Velocity *= pType->Trajectory_Speed / pBullet->Velocity.Magnitude();
-
- this->CalculateDisperseBurst(pBullet);
- this->RemainingDistance += static_cast(middleLocation.DistanceFrom(pBullet->SourceCoords) + pType->Trajectory_Speed);
- }
- else
- {
- this->IsFalling = true;
- auto middleLocation = CoordStruct::Empty;
-
- if (!pType->FreeFallOnTarget)
- {
- middleLocation = this->CalculateMiddleCoords(pBullet);
-
- pBullet->Velocity.X = static_cast(pBullet->TargetCoords.X - middleLocation.X);
- pBullet->Velocity.Y = static_cast(pBullet->TargetCoords.Y - middleLocation.Y);
- pBullet->Velocity.Z = static_cast(pBullet->TargetCoords.Z - middleLocation.Z);
- pBullet->Velocity *= pType->FallSpeed / pBullet->Velocity.Magnitude();
-
- this->CalculateDisperseBurst(pBullet);
- this->RemainingDistance += static_cast(pBullet->TargetCoords.DistanceFrom(middleLocation) + pType->FallSpeed);
- }
- else
- {
- middleLocation = CoordStruct { pBullet->TargetCoords.X, pBullet->TargetCoords.Y, static_cast(this->Height) };
- }
-
- const auto pExt = BulletExt::ExtMap.Find(pBullet);
-
- if (pExt->LaserTrails.size())
- {
- for (const auto& pTrail : pExt->LaserTrails)
- pTrail->LastLocation = middleLocation;
- }
- this->RefreshBulletLineTrail(pBullet);
-
- pBullet->SetLocation(middleLocation);
- const auto pTechno = pBullet->Owner;
- const auto pOwner = pTechno ? pTechno->Owner : pExt->FirerHouse;
- AnimExt::CreateRandomAnim(pType->TurningPointAnims, middleLocation, pTechno, pOwner, true);
- }
-}
-
-CoordStruct BombardTrajectory::CalculateMiddleCoords(BulletClass* pBullet)
-{
- const auto pType = this->Type;
- const auto length = ScenarioClass::Instance->Random.RandomRanged(pType->FallScatter_Min.Get(), pType->FallScatter_Max.Get());
- const auto vectorX = (pBullet->TargetCoords.X - pBullet->SourceCoords.X) * this->FallPercent;
- const auto vectorY = (pBullet->TargetCoords.Y - pBullet->SourceCoords.Y) * this->FallPercent;
- double scatterX = 0.0;
- double scatterY = 0.0;
-
- if (!pType->FallScatter_Linear)
- {
- const auto angel = ScenarioClass::Instance->Random.RandomDouble() * Math::TwoPi;
- scatterX = length * Math::cos(angel);
- scatterY = length * Math::sin(angel);
- }
- else
- {
- const auto vectorModule = sqrt(vectorX * vectorX + vectorY * vectorY);
- scatterX = vectorY / vectorModule * length;
- scatterY = -(vectorX / vectorModule * length);
-
- if (ScenarioClass::Instance->Random.RandomRanged(0, 1))
- {
- scatterX = -scatterX;
- scatterY = -scatterY;
- }
- }
-
- return CoordStruct
- {
- pBullet->SourceCoords.X + static_cast(vectorX + scatterX),
- pBullet->SourceCoords.Y + static_cast(vectorY + scatterY),
- static_cast(this->Height)
- };
-}
-
-void BombardTrajectory::CalculateTargetCoords(BulletClass* pBullet)
-{
- const auto pType = this->Type;
- auto theTargetCoords = pBullet->TargetCoords;
- const auto theSourceCoords = pBullet->SourceCoords;
-
- if (pType->NoLaunch)
- theTargetCoords += this->CalculateBulletLeadTime(pBullet);
-
- pBullet->TargetCoords = theTargetCoords;
-
- // Calculate the orientation of the coordinate system
- if (!pType->LeadTimeCalculate && theTargetCoords == theSourceCoords && pBullet->Owner) //For disperse.
- {
- const auto theOwnerCoords = pBullet->Owner->GetCoords();
- this->RotateAngle = Math::atan2(theTargetCoords.Y - theOwnerCoords.Y , theTargetCoords.X - theOwnerCoords.X);
- }
- else
- {
- this->RotateAngle = Math::atan2(theTargetCoords.Y - theSourceCoords.Y , theTargetCoords.X - theSourceCoords.X);
- }
-
- // Add the fixed offset value
- if (this->OffsetCoord != CoordStruct::Empty)
- {
- pBullet->TargetCoords.X += static_cast(this->OffsetCoord.X * Math::cos(this->RotateAngle) + this->OffsetCoord.Y * Math::sin(this->RotateAngle));
- pBullet->TargetCoords.Y += static_cast(this->OffsetCoord.X * Math::sin(this->RotateAngle) - this->OffsetCoord.Y * Math::cos(this->RotateAngle));
- pBullet->TargetCoords.Z += this->OffsetCoord.Z;
- }
-
- // Add random offset value
- if (pBullet->Type->Inaccurate)
- {
- const auto pTypeExt = BulletTypeExt::ExtMap.Find(pBullet->Type);
- const auto offsetMult = 0.0004 * pBullet->SourceCoords.DistanceFrom(pBullet->TargetCoords);
- const auto offsetMin = static_cast(offsetMult * pTypeExt->BallisticScatter_Min.Get(Leptons(0)));
- const auto offsetMax = static_cast(offsetMult * pTypeExt->BallisticScatter_Max.Get(Leptons(RulesClass::Instance->BallisticScatter)));
- const auto offsetDistance = ScenarioClass::Instance->Random.RandomRanged(offsetMin, offsetMax);
- pBullet->TargetCoords = MapClass::GetRandomCoordsNear(pBullet->TargetCoords, offsetDistance, false);
- }
-}
-
-CoordStruct BombardTrajectory::CalculateBulletLeadTime(BulletClass* pBullet)
-{
- const auto pType = this->Type;
- auto coords = CoordStruct::Empty;
-
- if (pType->LeadTimeCalculate)
- {
- if (const auto pTarget = pBullet->Target)
- {
- const auto theTargetCoords = pTarget->GetCoords();
- const auto theSourceCoords = pBullet->Location;
-
- // Solving trigonometric functions
- if (theTargetCoords != this->LastTargetCoord)
- {
- int travelTime = 0;
- const auto extraOffsetCoord = theTargetCoords - this->LastTargetCoord;
- const auto targetSourceCoord = theSourceCoords - theTargetCoords;
- const auto lastSourceCoord = theSourceCoords - this->LastTargetCoord;
-
- if (pType->FreeFallOnTarget)
- {
- travelTime += static_cast(sqrt(2 * (this->Height - theTargetCoords.Z) / BulletTypeExt::GetAdjustedGravity(pBullet->Type)));
- coords += extraOffsetCoord * (travelTime + 1);
- }
- else if (pType->NoLaunch)
- {
- travelTime += static_cast((this->Height - theTargetCoords.Z) / pType->FallSpeed);
- coords += extraOffsetCoord * (travelTime + 1);
- }
- else
- {
- const auto theDistanceSquared = targetSourceCoord.MagnitudeSquared();
- const auto targetSpeedSquared = extraOffsetCoord.MagnitudeSquared();
- const auto targetSpeed = sqrt(targetSpeedSquared);
-
- const auto crossFactor = lastSourceCoord.CrossProduct(targetSourceCoord).MagnitudeSquared();
- const auto verticalDistanceSquared = crossFactor / targetSpeedSquared;
-
- const auto horizonDistanceSquared = theDistanceSquared - verticalDistanceSquared;
- const auto horizonDistance = sqrt(horizonDistanceSquared);
-
- const auto straightSpeedSquared = pType->FallSpeed * pType->FallSpeed;
- const auto baseFactor = straightSpeedSquared - targetSpeedSquared;
- const auto squareFactor = baseFactor * verticalDistanceSquared + straightSpeedSquared * horizonDistanceSquared;
-
- // Is there a solution?
- if (squareFactor > 1e-10)
- {
- const auto minusFactor = -(horizonDistance * targetSpeed);
-
- if (std::abs(baseFactor) < 1e-10)
- {
- travelTime = std::abs(horizonDistance) > 1e-10 ? (static_cast(theDistanceSquared / (2 * horizonDistance * targetSpeed)) + 1) : 0;
- }
- else
- {
- const auto travelTimeM = static_cast((minusFactor - sqrt(squareFactor)) / baseFactor);
- const auto travelTimeP = static_cast((minusFactor + sqrt(squareFactor)) / baseFactor);
-
- if (travelTimeM > 0 && travelTimeP > 0)
- travelTime = travelTimeM < travelTimeP ? travelTimeM : travelTimeP;
- else if (travelTimeM > 0)
- travelTime = travelTimeM;
- else if (travelTimeP > 0)
- travelTime = travelTimeP;
-
- if (targetSourceCoord.MagnitudeSquared() < lastSourceCoord.MagnitudeSquared())
- travelTime += 1;
- else
- travelTime += 2;
- }
-
- coords += extraOffsetCoord * travelTime;
- }
- }
- }
- }
- }
-
- return coords;
-}
-
-void BombardTrajectory::CalculateDisperseBurst(BulletClass* pBullet)
-{
- const auto pType = this->Type;
-
- if (!this->UseDisperseBurst && std::abs(pType->RotateCoord) > 1e-10 && this->CountOfBurst > 1)
- {
- const auto axis = pType->AxisOfRotation.Get();
-
- BulletVelocity rotationAxis
- {
- axis.X * Math::cos(this->RotateAngle) + axis.Y * Math::sin(this->RotateAngle),
- axis.X * Math::sin(this->RotateAngle) - axis.Y * Math::cos(this->RotateAngle),
- static_cast(axis.Z)
- };
-
- const auto rotationAxisLengthSquared = rotationAxis.MagnitudeSquared();
-
- if (std::abs(rotationAxisLengthSquared) > 1e-10)
- {
- double extraRotate = 0.0;
- rotationAxis *= 1 / sqrt(rotationAxisLengthSquared);
-
- if (pType->MirrorCoord)
- {
- if (this->CurrentBurst % 2 == 1)
- rotationAxis *= -1;
-
- extraRotate = Math::Pi * (pType->RotateCoord * ((this->CurrentBurst / 2) / (this->CountOfBurst - 1.0) - 0.5)) / (this->IsFalling ? 90 : 180);
- }
- else
- {
- extraRotate = Math::Pi * (pType->RotateCoord * (this->CurrentBurst / (this->CountOfBurst - 1.0) - 0.5)) / (this->IsFalling ? 90 : 180);
- }
-
- const auto cosRotate = Math::cos(extraRotate);
- pBullet->Velocity = (pBullet->Velocity * cosRotate) + (rotationAxis * ((1 - cosRotate) * (pBullet->Velocity * rotationAxis))) + (rotationAxis.CrossProduct(pBullet->Velocity) * Math::sin(extraRotate));
- }
- }
-}
-
-bool BombardTrajectory::BulletPrepareCheck(BulletClass* pBullet)
-{
- // The time between bullets' Unlimbo() and Update() is completely uncertain.
- // Technos will update its location after firing, which may result in inaccurate
- // target position recorded by the LastTargetCoord in Unlimbo(). Therefore, it's
- // necessary to record the position during the first Update(). - CrimRecya
- if (this->WaitOneFrame == 2)
- {
- if (const auto pTarget = pBullet->Target)
- {
- this->LastTargetCoord = pTarget->GetCoords();
- this->WaitOneFrame = 1;
- return true;
- }
- }
-
- this->WaitOneFrame = 0;
- this->PrepareForOpenFire(pBullet);
-
- return false;
-}
-
-bool BombardTrajectory::BulletDetonatePreCheck(BulletClass* pBullet)
-{
- const auto pType = this->Type;
-
- // Close enough
- if (pBullet->TargetCoords.DistanceFrom(pBullet->Location) < pType->DetonationDistance.Get())
- return true;
-
- // Height
- if (pType->DetonationHeight >= 0)
- {
- if (pType->EarlyDetonation && (pBullet->Location.Z - pBullet->SourceCoords.Z) > pType->DetonationHeight)
- return true;
- else if (this->IsFalling && (pBullet->Location.Z - pBullet->SourceCoords.Z) < pType->DetonationHeight)
- return true;
- }
-
- // Ground, must be checked when free fall
- if (pType->SubjectToGround || (this->IsFalling && pType->FreeFallOnTarget))
- {
- if (MapClass::Instance.GetCellFloorHeight(pBullet->Location) >= (pBullet->Location.Z + 15))
- return true;
- }
-
- return false;
-}
-
-bool BombardTrajectory::BulletDetonateRemainCheck(BulletClass* pBullet)
-{
- const auto pType = this->Type;
- this->RemainingDistance -= static_cast(pType->FallSpeed);
-
- if (this->RemainingDistance < 0)
- return true;
-
- if (this->RemainingDistance < pType->FallSpeed)
- {
- pBullet->Velocity *= this->RemainingDistance / pType->FallSpeed;
- this->RemainingDistance = 0;
- }
-
- return false;
-}
-
-void BombardTrajectory::BulletVelocityChange(BulletClass* pBullet)
-{
- const auto pType = this->Type;
-
- if (!this->IsFalling)
- {
- this->RemainingDistance -= static_cast(pType->Trajectory_Speed);
-
- if (this->RemainingDistance < static_cast(pType->Trajectory_Speed))
- {
- if (this->ToFalling)
- {
- this->IsFalling = true;
- this->RemainingDistance = 1;
- const auto pTarget = pBullet->Target;
- auto middleLocation = CoordStruct::Empty;
-
- if (!pType->FreeFallOnTarget)
- {
- if (pType->LeadTimeCalculate && pTarget)
- pBullet->TargetCoords += pTarget->GetCoords() - this->InitialTargetCoord + this->CalculateBulletLeadTime(pBullet);
-
- middleLocation = pBullet->Location;
- pBullet->Velocity.X = static_cast(pBullet->TargetCoords.X - middleLocation.X);
- pBullet->Velocity.Y = static_cast(pBullet->TargetCoords.Y - middleLocation.Y);
- pBullet->Velocity.Z = static_cast(pBullet->TargetCoords.Z - middleLocation.Z);
- pBullet->Velocity *= pType->FallSpeed / pBullet->Velocity.Magnitude();
-
- this->CalculateDisperseBurst(pBullet);
- this->RemainingDistance += static_cast(pBullet->TargetCoords.DistanceFrom(middleLocation) + pType->FallSpeed);
- }
- else
- {
- if (pType->LeadTimeCalculate && pTarget)
- pBullet->TargetCoords += pTarget->GetCoords() - this->InitialTargetCoord + this->CalculateBulletLeadTime(pBullet);
-
- middleLocation = pBullet->TargetCoords;
- middleLocation.Z = pBullet->Location.Z;
-
- pBullet->Velocity = BulletVelocity::Empty;
- }
-
- const auto pExt = BulletExt::ExtMap.Find(pBullet);
-
- if (pExt->LaserTrails.size())
- {
- for (const auto& pTrail : pExt->LaserTrails)
- pTrail->LastLocation = middleLocation;
- }
-
- this->RefreshBulletLineTrail(pBullet);
-
- pBullet->SetLocation(middleLocation);
- const auto pTechno = pBullet->Owner;
- const auto pOwner = pTechno ? pTechno->Owner : pExt->FirerHouse;
- AnimExt::CreateRandomAnim(pType->TurningPointAnims, middleLocation, pTechno, pOwner, true);
- }
- else
- {
- this->ToFalling = true;
- const auto pTarget = pBullet->Target;
-
- if (pType->LeadTimeCalculate && pTarget)
- this->LastTargetCoord = pTarget->GetCoords();
-
- pBullet->Velocity *= this->RemainingDistance / pType->Trajectory_Speed;
- }
- }
- }
- else if (pType->FreeFallOnTarget)
- {
- pBullet->Velocity.Z -= BulletTypeExt::GetAdjustedGravity(pBullet->Type);
- }
-}
-
-void BombardTrajectory::RefreshBulletLineTrail(BulletClass* pBullet)
-{
- if (const auto pLineTrailer = pBullet->LineTrailer)
- {
- pLineTrailer->~LineTrail();
- pBullet->LineTrailer = nullptr;
- }
-
- const auto pType = pBullet->Type;
-
- if (pType->UseLineTrail)
- {
- const auto pLineTrailer = GameCreate();
- pBullet->LineTrailer = pLineTrailer;
-
- if (RulesClass::Instance->LineTrailColorOverride != ColorStruct { 0, 0, 0 })
- pLineTrailer->Color = RulesClass::Instance->LineTrailColorOverride;
- else
- pLineTrailer->Color = pType->LineTrailColor;
-
- pLineTrailer->SetDecrement(pType->LineTrailColorDecrement);
- pLineTrailer->Owner = pBullet;
- }
-}
diff --git a/src/Ext/Bullet/Trajectories/BombardTrajectory.h b/src/Ext/Bullet/Trajectories/BombardTrajectory.h
deleted file mode 100644
index 0486a1aac8..0000000000
--- a/src/Ext/Bullet/Trajectories/BombardTrajectory.h
+++ /dev/null
@@ -1,125 +0,0 @@
-#pragma once
-
-#include "PhobosTrajectory.h"
-
-class BombardTrajectoryType final : public PhobosTrajectoryType
-{
-public:
- BombardTrajectoryType() : PhobosTrajectoryType()
- , Height { 0.0 }
- , FallPercent { 1.0 }
- , FallPercentShift { 0.0 }
- , FallScatter_Max { Leptons(0) }
- , FallScatter_Min { Leptons(0) }
- , FallScatter_Linear { false }
- , FallSpeed { 0.0 }
- , DetonationDistance { Leptons(102) }
- , DetonationHeight { -1 }
- , EarlyDetonation { false }
- , TargetSnapDistance { Leptons(128) }
- , FreeFallOnTarget { true }
- , LeadTimeCalculate { false }
- , NoLaunch { false }
- , TurningPointAnims {}
- , OffsetCoord { { 0, 0, 0 } }
- , RotateCoord { 0 }
- , MirrorCoord { true }
- , UseDisperseBurst { false }
- , AxisOfRotation { { 0, 0, 1 } }
- , SubjectToGround { false }
- {}
-
- virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override;
- virtual bool Save(PhobosStreamWriter& Stm) const override;
- virtual std::unique_ptr CreateInstance() const override;
- virtual void Read(CCINIClass* const pINI, const char* pSection) override;
- virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Bombard; }
-
- Valueable Height;
- Valueable FallPercent;
- Valueable FallPercentShift;
- Valueable FallScatter_Max;
- Valueable FallScatter_Min;
- Valueable FallScatter_Linear;
- Valueable FallSpeed;
- Valueable DetonationDistance;
- Valueable DetonationHeight;
- Valueable EarlyDetonation;
- Valueable TargetSnapDistance;
- Valueable FreeFallOnTarget;
- Valueable LeadTimeCalculate;
- Valueable NoLaunch;
- ValueableVector