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 TurningPointAnims; - Valueable OffsetCoord; - Valueable RotateCoord; - Valueable MirrorCoord; - Valueable UseDisperseBurst; - Valueable AxisOfRotation; - Valueable SubjectToGround; - -private: - template - void Serialize(T& Stm); -}; - -class BombardTrajectory final : public PhobosTrajectory -{ -public: - BombardTrajectory(noinit_t) { } - - BombardTrajectory(BombardTrajectoryType const* trajType) : Type { trajType } - , Height { trajType->Height } - , FallPercent { trajType->FallPercent - trajType->FallPercentShift } - , OffsetCoord { trajType->OffsetCoord.Get() } - , UseDisperseBurst { trajType->UseDisperseBurst } - , IsFalling { false } - , ToFalling { false } - , RemainingDistance { 1 } - , LastTargetCoord {} - , InitialTargetCoord {} - , CountOfBurst { 0 } - , CurrentBurst { 0 } - , RotateAngle { 0 } - , WaitOneFrame { 0 } - {} - - 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(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) override; - virtual bool OnAI(BulletClass* pBullet) override; - virtual void OnAIPreDetonate(BulletClass* pBullet) override; - virtual void OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) override; - virtual TrajectoryCheckReturnType OnAITargetCoordCheck(BulletClass* pBullet) override; - virtual TrajectoryCheckReturnType OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) override; - - const BombardTrajectoryType* Type; - double Height; - double FallPercent; - CoordStruct OffsetCoord; - bool UseDisperseBurst; - bool IsFalling; - bool ToFalling; - int RemainingDistance; - CoordStruct LastTargetCoord; - CoordStruct InitialTargetCoord; - int CountOfBurst; - int CurrentBurst; - double RotateAngle; - int WaitOneFrame; - -private: - template - void Serialize(T& Stm); - - void PrepareForOpenFire(BulletClass* pBullet); - CoordStruct CalculateMiddleCoords(BulletClass* pBullet); - void CalculateTargetCoords(BulletClass* pBullet); - CoordStruct CalculateBulletLeadTime(BulletClass* pBullet); - void CalculateDisperseBurst(BulletClass* pBullet); - bool BulletPrepareCheck(BulletClass* pBullet); - bool BulletDetonatePreCheck(BulletClass* pBullet); - bool BulletDetonateRemainCheck(BulletClass* pBullet); - void BulletVelocityChange(BulletClass* pBullet); - void RefreshBulletLineTrail(BulletClass* pBullet); -}; diff --git a/src/Ext/Bullet/Trajectories/ParabolaTrajectory.cpp b/src/Ext/Bullet/Trajectories/ParabolaTrajectory.cpp deleted file mode 100644 index 100da59b9b..0000000000 --- a/src/Ext/Bullet/Trajectories/ParabolaTrajectory.cpp +++ /dev/null @@ -1,1145 +0,0 @@ -#include "ParabolaTrajectory.h" - -#include -#include - -#include -#include -#include - -std::unique_ptr ParabolaTrajectoryType::CreateInstance() const -{ - return std::make_unique(this); -} - -template -void ParabolaTrajectoryType::Serialize(T& Stm) -{ - Stm - .Process(this->DetonationDistance) - .Process(this->TargetSnapDistance) - .Process(this->OpenFireMode) - .Process(this->ThrowHeight) - .Process(this->LaunchAngle) - .Process(this->LeadTimeCalculate) - .Process(this->DetonationAngle) - .Process(this->DetonationHeight) - .Process(this->BounceTimes) - .Process(this->BounceOnWater) - .Process(this->BounceDetonate) - .Process(this->BounceAttenuation) - .Process(this->BounceCoefficient) - .Process(this->OffsetCoord) - .Process(this->RotateCoord) - .Process(this->MirrorCoord) - .Process(this->UseDisperseBurst) - .Process(this->AxisOfRotation) - ; -} - -bool ParabolaTrajectoryType::Load(PhobosStreamReader& Stm, bool RegisterForChange) -{ - this->PhobosTrajectoryType::Load(Stm, false); - this->Serialize(Stm); - return true; -} - -bool ParabolaTrajectoryType::Save(PhobosStreamWriter& Stm) const -{ - this->PhobosTrajectoryType::Save(Stm); - const_cast(this)->Serialize(Stm); - return true; -} - -void ParabolaTrajectoryType::Read(CCINIClass* const pINI, const char* pSection) -{ - INI_EX exINI(pINI); - - this->DetonationDistance.Read(exINI, pSection, "Trajectory.Parabola.DetonationDistance"); - this->TargetSnapDistance.Read(exINI, pSection, "Trajectory.Parabola.TargetSnapDistance"); - - pINI->ReadString(pSection, "Trajectory.Parabola.OpenFireMode", "", Phobos::readBuffer); - if (INIClass::IsBlank(Phobos::readBuffer)) - this->OpenFireMode = ParabolaFireMode::Speed; - else if (_stricmp(Phobos::readBuffer, "height") == 0) - this->OpenFireMode = ParabolaFireMode::Height; - else if (_stricmp(Phobos::readBuffer, "angle") == 0) - this->OpenFireMode = ParabolaFireMode::Angle; - else if (_stricmp(Phobos::readBuffer, "speedandheight") == 0) - this->OpenFireMode = ParabolaFireMode::SpeedAndHeight; - else if (_stricmp(Phobos::readBuffer, "heightandangle") == 0) - this->OpenFireMode = ParabolaFireMode::HeightAndAngle; - else if (_stricmp(Phobos::readBuffer, "speedandangle") == 0) - this->OpenFireMode = ParabolaFireMode::SpeedAndAngle; - else - this->OpenFireMode = ParabolaFireMode::Speed; - - this->ThrowHeight.Read(exINI, pSection, "Trajectory.Parabola.ThrowHeight"); - this->LaunchAngle.Read(exINI, pSection, "Trajectory.Parabola.LaunchAngle"); - this->LeadTimeCalculate.Read(exINI, pSection, "Trajectory.Parabola.LeadTimeCalculate"); - this->DetonationAngle.Read(exINI, pSection, "Trajectory.Parabola.DetonationAngle"); - this->DetonationHeight.Read(exINI, pSection, "Trajectory.Parabola.DetonationHeight"); - this->BounceTimes.Read(exINI, pSection, "Trajectory.Parabola.BounceTimes"); - this->BounceOnWater.Read(exINI, pSection, "Trajectory.Parabola.BounceOnWater"); - this->BounceDetonate.Read(exINI, pSection, "Trajectory.Parabola.BounceDetonate"); - this->BounceAttenuation.Read(exINI, pSection, "Trajectory.Parabola.BounceAttenuation"); - this->BounceCoefficient.Read(exINI, pSection, "Trajectory.Parabola.BounceCoefficient"); - this->OffsetCoord.Read(exINI, pSection, "Trajectory.Parabola.OffsetCoord"); - this->RotateCoord.Read(exINI, pSection, "Trajectory.Parabola.RotateCoord"); - this->MirrorCoord.Read(exINI, pSection, "Trajectory.Parabola.MirrorCoord"); - this->UseDisperseBurst.Read(exINI, pSection, "Trajectory.Parabola.UseDisperseBurst"); - this->AxisOfRotation.Read(exINI, pSection, "Trajectory.Parabola.AxisOfRotation"); -} - -template -void ParabolaTrajectory::Serialize(T& Stm) -{ - Stm - .Process(this->Type) - .Process(this->ThrowHeight) - .Process(this->BounceTimes) - .Process(this->OffsetCoord) - .Process(this->UseDisperseBurst) - .Process(this->ShouldDetonate) - .Process(this->ShouldBounce) - .Process(this->NeedExtraCheck) - .Process(this->LastTargetCoord) - .Process(this->CurrentBurst) - .Process(this->CountOfBurst) - .Process(this->WaitOneFrame) - .Process(this->LastVelocity) - ; -} - -bool ParabolaTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange) -{ - this->Serialize(Stm); - return true; -} - -bool ParabolaTrajectory::Save(PhobosStreamWriter& Stm) const -{ - const_cast(this)->Serialize(Stm); - return true; -} - -void ParabolaTrajectory::OnUnlimbo(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) -{ - const auto pType = this->Type; - this->LastTargetCoord = pBullet->TargetCoords; - pBullet->Velocity = BulletVelocity::Empty; - const auto pTarget = abstract_cast(pBullet->Target); - bool resetTarget = false; - - // Special case: Set the target to the ground - if (pType->DetonationDistance.Get() <= -1e-10 && pTarget) - { - if (const auto pCell = MapClass::Instance.TryGetCellAt(pTarget->GetCoords())) - { - pBullet->Target = pCell; - pBullet->TargetCoords = pCell->GetCoords(); - resetTarget = true; - } - } - - // Record some information, and try to see if mirror offset is needed like Straight - if (const auto pWeapon = pBullet->WeaponType) - this->CountOfBurst = pWeapon->Burst; - - if (const auto pFirer = pBullet->Owner) - { - this->CurrentBurst = pFirer->CurrentBurstIndex; - - if (pType->MirrorCoord && pFirer->CurrentBurstIndex % 2 == 1) - this->OffsetCoord.Y = -(this->OffsetCoord.Y); - } - - // Wait, or launch immediately? - if (!pType->LeadTimeCalculate || !pTarget || resetTarget) - this->PrepareForOpenFire(pBullet); - else - this->WaitOneFrame = 2; -} - -bool ParabolaTrajectory::OnAI(BulletClass* pBullet) -{ - if (this->WaitOneFrame && this->BulletPrepareCheck(pBullet)) - return false; - - if (this->BulletDetonatePreCheck(pBullet)) - return true; - - const auto pCell = MapClass::Instance.TryGetCellAt(pBullet->Location); - const auto bounce = this->ShouldBounce; - - if (!pCell || (bounce && this->CalculateBulletVelocityAfterBounce(pBullet, pCell))) - return true; - - return this->BulletDetonateLastCheck(pBullet, pCell, BulletTypeExt::GetAdjustedGravity(pBullet->Type), bounce); -} - -void ParabolaTrajectory::OnAIPreDetonate(BulletClass* pBullet) -{ - const auto targetSnapDistance = this->Type->TargetSnapDistance.Get(); - - // Whether to snap to target? - if (targetSnapDistance > 0) - { - const auto pTarget = abstract_cast(pBullet->Target); - const auto coords = pTarget ? pTarget->GetCoords() : pBullet->Data.Location; - - if (coords.DistanceFrom(pBullet->Location) <= targetSnapDistance) - { - const auto pExt = BulletExt::ExtMap.Find(pBullet); - pExt->SnappedToTarget = true; - pBullet->SetLocation(coords); - return; - } - } - - // If the speed is too fast, it may smash through the floor - const auto cellHeight = MapClass::Instance.GetCellFloorHeight(pBullet->Location); - - if (pBullet->Location.Z < cellHeight) - pBullet->SetLocation(CoordStruct{ pBullet->Location.X, pBullet->Location.Y, cellHeight }); -} - -void ParabolaTrajectory::OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) -{ - pSpeed->Z += BulletTypeExt::GetAdjustedGravity(pBullet->Type); // Seems like this is useless -} - -TrajectoryCheckReturnType ParabolaTrajectory::OnAITargetCoordCheck(BulletClass* pBullet) -{ - return TrajectoryCheckReturnType::SkipGameCheck; -} - -TrajectoryCheckReturnType ParabolaTrajectory::OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) -{ - return TrajectoryCheckReturnType::SkipGameCheck; -} - -void ParabolaTrajectory::PrepareForOpenFire(BulletClass* pBullet) -{ - const auto pType = this->Type; - const auto pTarget = pBullet->Target; - bool leadTimeCalculate = pType->LeadTimeCalculate && pTarget; - auto theTargetCoords = leadTimeCalculate ? pTarget->GetCoords() : pBullet->TargetCoords; - auto theSourceCoords = leadTimeCalculate ? pBullet->Location : pBullet->SourceCoords; - leadTimeCalculate &= theTargetCoords != this->LastTargetCoord; - double rotateAngle = 0.0; - - // Calculate the orientation of the coordinate system - if (!pType->LeadTimeCalculate && theTargetCoords == theSourceCoords && pBullet->Owner) //For disperse. - { - const auto theOwnerCoords = pBullet->Owner->GetCoords(); - rotateAngle = Math::atan2(theTargetCoords.Y - theOwnerCoords.Y , theTargetCoords.X - theOwnerCoords.X); - } - else - { - rotateAngle = Math::atan2(theTargetCoords.Y - theSourceCoords.Y , theTargetCoords.X - theSourceCoords.X); - } - - // Add the fixed offset value - if (this->OffsetCoord != CoordStruct::Empty) - { - theTargetCoords.X += static_cast(this->OffsetCoord.X * Math::cos(rotateAngle) + this->OffsetCoord.Y * Math::sin(rotateAngle)); - theTargetCoords.Y += static_cast(this->OffsetCoord.X * Math::sin(rotateAngle) - this->OffsetCoord.Y * Math::cos(rotateAngle)); - theTargetCoords.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 * theSourceCoords.DistanceFrom(theTargetCoords); - 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); - theTargetCoords = MapClass::GetRandomCoordsNear(theTargetCoords, offsetDistance, false); - } - - pBullet->TargetCoords = theTargetCoords; - - // Non positive gravity is not accepted - const auto gravity = BulletTypeExt::GetAdjustedGravity(pBullet->Type); - - if (gravity <= 1e-10) - { - pBullet->Velocity = BulletVelocity::Empty; - this->ShouldDetonate = true; - return; - } - - // Calculate the firing velocity vector of the bullet - if (leadTimeCalculate) - this->CalculateBulletVelocityLeadTime(pBullet, &theSourceCoords, gravity); - else - this->CalculateBulletVelocityRightNow(pBullet, &theSourceCoords, gravity); - - // Rotate the selected angle - if (!this->UseDisperseBurst && std::abs(pType->RotateCoord) > 1e-10 && this->CountOfBurst > 1) - { - const auto axis = pType->AxisOfRotation.Get(); - - BulletVelocity rotationAxis - { - axis.X * Math::cos(rotateAngle) + axis.Y * Math::sin(rotateAngle), - axis.X * Math::sin(rotateAngle) - axis.Y * Math::cos(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)) / 180; - } - else - { - extraRotate = Math::Pi * (pType->RotateCoord * (this->CurrentBurst / (this->CountOfBurst - 1.0) - 0.5)) / 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 ParabolaTrajectory::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; -} - -void ParabolaTrajectory::CalculateBulletVelocityLeadTime(BulletClass* pBullet, CoordStruct* pSourceCoords, double gravity) -{ - const auto pType = this->Type; - auto targetCoords = pBullet->Target->GetCoords(); - auto offsetCoords = pBullet->TargetCoords - targetCoords; - - 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 auto meetTime = this->SearchFixedHeightMeetTime(pSourceCoords, &targetCoords, &offsetCoords, gravity); - - // Step 2: Substitute the time into the calculation of the attack coordinates - pBullet->TargetCoords += (targetCoords - this->LastTargetCoord) * meetTime; - const auto destinationCoords = pBullet->TargetCoords - *pSourceCoords; - - // Step 3: Check if it is an unsolvable solution - if (meetTime <= 1e-10 || destinationCoords.Magnitude() <= 1e-10) - break; - - // Step 4: Calculate the horizontal component of the projectile velocity - pBullet->Velocity.X = destinationCoords.X / meetTime; - pBullet->Velocity.Y = destinationCoords.Y / meetTime; - - // Step 5: Determine the maximum height that the projectile should reach - const auto sourceHeight = pSourceCoords->Z; - const auto targetHeight = sourceHeight + destinationCoords.Z; - const auto maxHeight = destinationCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight; - - // Step 6: Calculate the vertical component of the projectile velocity - pBullet->Velocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight)) + gravity / 2; - - // Step 7: Record whether it requires additional checks during the flight - this->CheckIfNeedExtraCheck(pBullet); - return; - } - case ParabolaFireMode::Angle: // Fixed fire angle and aim at the target - { - // Step 1: Read the appropriate fire angle - auto 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 auto meetTime = this->SearchFixedAngleMeetTime(pSourceCoords, &targetCoords, &offsetCoords, radian, gravity); - - // Step 3: Substitute the time into the calculation of the attack coordinates - pBullet->TargetCoords += (targetCoords - this->LastTargetCoord) * meetTime; - const auto destinationCoords = pBullet->TargetCoords - *pSourceCoords; - - // Step 4: Check if it is an unsolvable solution - if (meetTime <= 1e-10 || destinationCoords.Magnitude() <= 1e-10) - break; - - // Step 5: Calculate each horizontal component of the projectile velocity - pBullet->Velocity.X = destinationCoords.X / meetTime; - pBullet->Velocity.Y = destinationCoords.Y / meetTime; - - // Step 6: Calculate whole horizontal component of the projectile velocity - const auto horizontalDistance = Point2D { destinationCoords.X, destinationCoords.Y }.Magnitude(); - const auto horizontalVelocity = horizontalDistance / meetTime; - - // Step 7: Calculate the vertical component of the projectile velocity - pBullet->Velocity.Z = horizontalVelocity * Math::tan(radian) + gravity / 2; - - // Step 8: Record whether it requires additional checks during the flight - this->CheckIfNeedExtraCheck(pBullet); - 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 auto meetTime = this->SolveFixedSpeedMeetTime(pSourceCoords, &targetCoords, &offsetCoords, pType->Trajectory_Speed); - - // Step 2: Substitute the time into the calculation of the attack coordinates - pBullet->TargetCoords += (targetCoords - this->LastTargetCoord) * meetTime; - const CoordStruct destinationCoords = pBullet->TargetCoords - *pSourceCoords; - - // Step 3: Check if it is an unsolvable solution - if (meetTime <= 1e-10 || destinationCoords.Magnitude() <= 1e-10) - break; - - // Step 4: Calculate the ratio of horizontal velocity to horizontal distance - const auto horizontalDistance = Point2D { destinationCoords.X, destinationCoords.Y }.Magnitude(); - const auto mult = horizontalDistance > 1e-10 ? pType->Trajectory_Speed / horizontalDistance : 1.0; - - // Step 5: Calculate the horizontal component of the projectile velocity - pBullet->Velocity.X = destinationCoords.X * mult; - pBullet->Velocity.Y = destinationCoords.Y * mult; - - // Step 6: Determine the maximum height that the projectile should reach - const auto sourceHeight = pSourceCoords->Z; - const auto targetHeight = sourceHeight + destinationCoords.Z; - const auto maxHeight = destinationCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight; - - // Step 7: Calculate the vertical component of the projectile velocity - pBullet->Velocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight)) + gravity / 2; - - // Step 8: Record whether it requires additional checks during the flight - this->CheckIfNeedExtraCheck(pBullet); - 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 auto meetTime = this->SearchFixedHeightMeetTime(pSourceCoords, &targetCoords, &offsetCoords, gravity); - - // Step 2: Substitute the time into the calculation of the attack coordinates - pBullet->TargetCoords += (targetCoords - this->LastTargetCoord) * meetTime; - const auto destinationCoords = pBullet->TargetCoords - *pSourceCoords; - - // Step 3: Check if it is an unsolvable solution - if (meetTime <= 1e-10 || destinationCoords.Magnitude() <= 1e-10) - break; - - // Step 4: Determine the maximum height that the projectile should reach - const auto sourceHeight = pSourceCoords->Z; - const auto targetHeight = sourceHeight + destinationCoords.Z; - const auto maxHeight = destinationCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight; - - // Step 5: Calculate the vertical component of the projectile velocity - pBullet->Velocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight)) + gravity / 2; - - // Step 6: Read the appropriate fire angle - auto radian = pType->LaunchAngle * Math::Pi / 180.0; - radian = (radian >= Math::HalfPi || radian <= 1e-10) ? (Math::HalfPi / 3) : radian; - - // Step 7: Calculate the ratio of horizontal velocity to horizontal distance - const auto horizontalDistance = Point2D { destinationCoords.X, destinationCoords.Y }.Magnitude(); - const auto mult = (pBullet->Velocity.Z / Math::tan(radian)) / horizontalDistance; - - // Step 8: Calculate the horizontal component of the projectile velocity - pBullet->Velocity.X = destinationCoords.X * mult; - pBullet->Velocity.Y = destinationCoords.Y * mult; - - // Step 9: Record whether it requires additional checks during the flight - this->CheckIfNeedExtraCheck(pBullet); - 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 auto meetTime = this->SolveFixedSpeedMeetTime(pSourceCoords, &targetCoords, &offsetCoords, pType->Trajectory_Speed); - - // Step 2: Substitute the time into the calculation of the attack coordinates - pBullet->TargetCoords += (targetCoords - this->LastTargetCoord) * meetTime; - const auto destinationCoords = pBullet->TargetCoords - *pSourceCoords; - - // Step 3: Check if it is an unsolvable solution - if (meetTime <= 1e-10 || destinationCoords.Magnitude() <= 1e-10) - break; - - // Step 4: Calculate the ratio of horizontal velocity to horizontal distance - const auto horizontalDistance = Point2D { destinationCoords.X, destinationCoords.Y }.Magnitude(); - const auto mult = horizontalDistance > 1e-10 ? pType->Trajectory_Speed / horizontalDistance : 1.0; - - // Step 5: Calculate each horizontal component of the projectile velocity - pBullet->Velocity.X = destinationCoords.X * mult; - pBullet->Velocity.Y = destinationCoords.Y * mult; - - // Step 6: Calculate whole horizontal component of the projectile velocity - const auto horizontalVelocity = horizontalDistance * mult; - - // Step 7: Read the appropriate fire angle - auto 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 - pBullet->Velocity.Z = horizontalVelocity * Math::tan(radian) + gravity / 2; - - // Step 9: Record whether it requires additional checks during the flight - this->CheckIfNeedExtraCheck(pBullet); - 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 auto meetTime = this->SolveFixedSpeedMeetTime(pSourceCoords, &targetCoords, &offsetCoords, pType->Trajectory_Speed); - - // Step 2: Substitute the time into the calculation of the attack coordinates - pBullet->TargetCoords += (targetCoords - this->LastTargetCoord) * meetTime; - const auto destinationCoords = pBullet->TargetCoords - *pSourceCoords; - - // Step 3: Check if it is an unsolvable solution - if (meetTime <= 1e-10 || destinationCoords.Magnitude() <= 1e-10) - break; - - // Step 4: Calculate the ratio of horizontal velocity to horizontal distance - const auto horizontalDistance = Point2D { destinationCoords.X, destinationCoords.Y }.Magnitude(); - const auto mult = horizontalDistance > 1e-10 ? pType->Trajectory_Speed / horizontalDistance : 1.0; - - // Step 5: Calculate the projectile velocity - pBullet->Velocity.X = destinationCoords.X * mult; - pBullet->Velocity.Y = destinationCoords.Y * mult; - pBullet->Velocity.Z = destinationCoords.Z * mult + (gravity * horizontalDistance) / (2 * pType->Trajectory_Speed) + gravity / 2; - - // Step 6: Record whether it requires additional checks during the flight - this->CheckIfNeedExtraCheck(pBullet); - return; - } - } - - // Reset target position - pBullet->TargetCoords = targetCoords + offsetCoords; - - // Substitute into the no lead time algorithm - this->CalculateBulletVelocityRightNow(pBullet, pSourceCoords, gravity); -} - -void ParabolaTrajectory::CalculateBulletVelocityRightNow(BulletClass* pBullet, CoordStruct* pSourceCoords, double gravity) -{ - const auto pType = this->Type; - // Calculate horizontal distance - const auto distanceCoords = pBullet->TargetCoords - *pSourceCoords; - const auto distance = distanceCoords.Magnitude(); - const auto horizontalDistance = Point2D { distanceCoords.X, distanceCoords.Y }.Magnitude(); - - if (distance <= 1e-10) - { - pBullet->Velocity = BulletVelocity::Empty; - this->ShouldDetonate = true; - 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 auto sourceHeight = pSourceCoords->Z; - const auto targetHeight = pBullet->TargetCoords.Z; - const auto maxHeight = distanceCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight; - - // Step 2: Calculate the vertical component of the projectile velocity - pBullet->Velocity.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 auto meetTime = sqrt(2 * (maxHeight - sourceHeight) / gravity) + sqrt(2 * (maxHeight - targetHeight) / gravity); - - // Step 4: Calculate the horizontal component of the projectile velocity - pBullet->Velocity.X = distanceCoords.X / meetTime; - pBullet->Velocity.Y = distanceCoords.Y / meetTime; - break; - } - case ParabolaFireMode::Angle: // Fixed fire angle and aim at the target - { - // Step 1: Read the appropriate fire angle - const auto radian = pType->LaunchAngle * Math::Pi / 180.0; - - // Step 2: Using Newton Iteration Method to determine the projectile velocity - const auto 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 - pBullet->Velocity.Z = velocity * Math::sin(radian); - - // Step 4: Calculate the ratio of horizontal velocity to horizontal distance - const auto mult = velocity * Math::cos(radian) / horizontalDistance; - - // Step 5: Calculate the horizontal component of the projectile velocity - pBullet->Velocity.X = distanceCoords.X * mult; - pBullet->Velocity.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 auto sourceHeight = pSourceCoords->Z; - const auto targetHeight = pBullet->TargetCoords.Z; - const auto maxHeight = distanceCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight; - - // Step 2: Calculate the vertical component of the projectile velocity - pBullet->Velocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight)); - - // Step 3: Calculate the ratio of horizontal velocity to horizontal distance - const auto mult = horizontalDistance > 1e-10 ? pType->Trajectory_Speed / horizontalDistance : 1.0; - - // Step 4: Calculate the horizontal component of the projectile velocity - pBullet->Velocity.X = distanceCoords.X * mult; - pBullet->Velocity.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 auto sourceHeight = pSourceCoords->Z; - const auto targetHeight = pBullet->TargetCoords.Z; - const auto maxHeight = distanceCoords.Z > 0 ? this->ThrowHeight + targetHeight : this->ThrowHeight + sourceHeight; - - // Step 2: Calculate the vertical component of the projectile velocity - pBullet->Velocity.Z = sqrt(2 * gravity * (maxHeight - sourceHeight)); - - // Step 3: Read the appropriate fire angle - auto radian = pType->LaunchAngle * Math::Pi / 180.0; - radian = (radian >= Math::HalfPi || radian <= 1e-10) ? (Math::HalfPi / 3) : radian; - - // Step 4: Calculate the ratio of horizontal velocity to horizontal distance - const auto mult = (pBullet->Velocity.Z / Math::tan(radian)) / horizontalDistance; - - // Step 5: Calculate the horizontal component of the projectile velocity - pBullet->Velocity.X = distanceCoords.X * mult; - pBullet->Velocity.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 auto mult = horizontalDistance > 1e-10 ? pType->Trajectory_Speed / horizontalDistance : 1.0; - - // Step 2: Calculate the horizontal component of the projectile velocity - pBullet->Velocity.X = distanceCoords.X * mult; - pBullet->Velocity.Y = distanceCoords.Y * mult; - - // Step 3: Read the appropriate fire angle - auto 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 - pBullet->Velocity.Z = pType->Trajectory_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 auto mult = horizontalDistance > 1e-10 ? pType->Trajectory_Speed / horizontalDistance : 1.0; - - // Step 2: Calculate the projectile velocity - pBullet->Velocity.X = distanceCoords.X * mult; - pBullet->Velocity.Y = distanceCoords.Y * mult; - pBullet->Velocity.Z = distanceCoords.Z * mult + (gravity * horizontalDistance) / (2 * pType->Trajectory_Speed); - break; - } - } - - // Record whether it requires additional checks during the flight - this->CheckIfNeedExtraCheck(pBullet); - - // Offset the gravity effect of the first time update - pBullet->Velocity.Z += gravity / 2; -} - -void ParabolaTrajectory::CheckIfNeedExtraCheck(BulletClass* pBullet) -{ - const auto pType = this->Type; - - switch (pType->OpenFireMode) - { - case ParabolaFireMode::Height: // Fixed max height and aim at the target - case ParabolaFireMode::Angle: // Fixed fire angle and aim at the target - case ParabolaFireMode::HeightAndAngle: // Fixed max height and fixed fire angle - { - this->NeedExtraCheck = Vector2D{ pBullet->Velocity.X, pBullet->Velocity.Y }.MagnitudeSquared() > 65536.0; - break; - } - default: // Fixed horizontal speed and blabla - { - this->NeedExtraCheck = pType->Trajectory_Speed > 256.0; - break; - } - } -} - -double ParabolaTrajectory::SearchVelocity(double horizontalDistance, int distanceCoordsZ, double radian, double gravity) -{ - // Estimate initial velocity - const auto mult = Math::sin(2 * radian); - auto velocity = std::abs(mult) > 1e-10 ? sqrt(horizontalDistance * gravity / mult) : 0.0; - velocity += distanceCoordsZ / gravity; - velocity = velocity > 8.0 ? velocity : 8.0; - const auto error = velocity / 16; - - // Step size - const auto delta = 1e-5; - - // Newton Iteration Method - for (int i = 0; i < 10; ++i) - { - // Substitute into the estimate speed - const auto differential = this->CheckVelocityEquation(horizontalDistance, distanceCoordsZ, velocity, radian, gravity); - const auto dDifferential = (this->CheckVelocityEquation(horizontalDistance, distanceCoordsZ, (velocity + delta), radian, gravity) - differential) / delta; - - // Check unacceptable divisor - if (std::abs(dDifferential) < 1e-10) - return velocity; - - // Calculate the speed of the next iteration - const auto difference = differential / dDifferential; - const auto 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(double horizontalDistance, int distanceCoordsZ, double velocity, double radian, double gravity) -{ - // Calculate each component of the projectile velocity - const auto horizontalVelocity = velocity * Math::cos(radian); - const auto verticalVelocity = velocity * Math::sin(radian); - - // Calculate the time of the rising phase - const auto upTime = verticalVelocity / gravity; - - // Calculate the maximum height that the projectile can reach - const auto maxHeight = 0.5 * verticalVelocity * upTime; - - // Calculate the time of the descent phase - const auto downTime = sqrt(2 * (maxHeight - distanceCoordsZ) / gravity); - - // Calculate the total time required for horizontal movement - const auto 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(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double horizontalSpeed) -{ - // Project all conditions onto a horizontal plane - const Point2D targetSpeedCrd { pTargetCrd->X - this->LastTargetCoord.X, pTargetCrd->Y - this->LastTargetCoord.Y }; - const Point2D destinationCrd { pTargetCrd->X + pOffsetCrd->X - pSourceCrd->X, pTargetCrd->Y + pOffsetCrd->Y - pSourceCrd->Y }; - - // Establishing a quadratic equation using time as a variable: - // (destinationCrd + targetSpeedCrd * time).Magnitude() = horizontalSpeed * time - - // Solve this quadratic equation - const auto divisor = (targetSpeedCrd.MagnitudeSquared() - horizontalSpeed * horizontalSpeed) * 2; - const auto factor = 2 * (targetSpeedCrd * destinationCrd); - const auto delta = factor * factor - 2 * divisor * destinationCrd.MagnitudeSquared(); - - if (delta >= 1e-10) - { - const auto timeP = (-factor + sqrt(delta)) / divisor; - const auto timeM = (-factor - sqrt(delta)) / divisor; - - if (timeM > 1e-10 && timeP > 1e-10) - return timeM < timeP ? timeM : timeP; - else if (timeM > 1e-10) - return timeM; - else if (timeP > 1e-10) - return timeP; - } - - return -1.0; -} - -double ParabolaTrajectory::SearchFixedHeightMeetTime(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double gravity) -{ - // Similar to method SearchVelocity, no further elaboration will be provided - const auto delta = 1e-5; - auto meetTime = (this->ThrowHeight << 2) / gravity; - - for (int i = 0; i < 10; ++i) - { - const auto differential = this->CheckFixedHeightEquation(pSourceCrd, pTargetCrd, pOffsetCrd, meetTime, gravity); - const auto dDifferential = (this->CheckFixedHeightEquation(pSourceCrd, pTargetCrd, pOffsetCrd, (meetTime + delta), gravity) - differential) / delta; - - if (std::abs(dDifferential) < 1e-10) - return meetTime; - - const auto difference = differential / dDifferential; - const auto meetTimeNew = meetTime - difference; - - if (std::abs(difference) < 1.0) - return meetTimeNew; - - meetTime = meetTimeNew; - } - - return -1.0; -} - -double ParabolaTrajectory::CheckFixedHeightEquation(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double meetTime, double gravity) -{ - // Calculate how high the target will reach during this period of time - const auto meetHeight = static_cast((pTargetCrd->Z - this->LastTargetCoord.Z) * meetTime) + pTargetCrd->Z + pOffsetCrd->Z; - - // Calculate how high the projectile can fly during this period of time - const auto maxHeight = meetHeight > pSourceCrd->Z ? this->ThrowHeight + meetHeight : this->ThrowHeight + pSourceCrd->Z; - - // Calculate the difference between these two times - return sqrt((maxHeight - pSourceCrd->Z) * 2 / gravity) + sqrt((maxHeight - meetHeight) * 2 / gravity) - meetTime; -} - -double ParabolaTrajectory::SearchFixedAngleMeetTime(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double radian, double gravity) -{ - // Similar to method SearchVelocity, no further elaboration will be provided - const auto delta = 1e-5; - auto meetTime = 512 * Math::sin(radian) / gravity; - - for (int i = 0; i < 10; ++i) - { - const auto differential = this->CheckFixedAngleEquation(pSourceCrd, pTargetCrd, pOffsetCrd, meetTime, radian, gravity); - const auto dDifferential = (this->CheckFixedAngleEquation(pSourceCrd, pTargetCrd, pOffsetCrd, (meetTime + delta), radian, gravity) - differential) / delta; - - if (std::abs(dDifferential) < 1e-10) - return meetTime; - - const auto difference = differential / dDifferential; - const auto meetTimeNew = meetTime - difference; - - if (std::abs(difference) < 1.0) - return meetTimeNew; - - meetTime = meetTimeNew; - } - - return -1.0; -} - -double ParabolaTrajectory::CheckFixedAngleEquation(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double meetTime, double radian, double gravity) -{ - // Using the estimated time to obtain the predicted location of the target - const auto distanceCoords = (*pTargetCrd - this->LastTargetCoord) * meetTime + *pTargetCrd + *pOffsetCrd - *pSourceCrd; - - // Calculate the horizontal distance between the target and the calculation - const auto horizontalDistance = Point2D{ distanceCoords.X, distanceCoords.Y }.Magnitude(); - - // Calculate the horizontal velocity - const auto horizontalVelocity = horizontalDistance / meetTime; - - // Calculate the vertical velocity - const auto verticalVelocity = horizontalVelocity * Math::tan(radian); - - // Calculate the time of the rising phase - const auto upTime = verticalVelocity / gravity; - - // Calculate the maximum height that the projectile can reach - const auto maxHeight = 0.5 * verticalVelocity * upTime; - - // Calculate the time of the descent phase - const auto 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(BulletClass* pBullet, CellClass* pCell) -{ - const auto pType = this->Type; - - // Can bounce on water surface? - if (pCell->LandType == LandType::Water && !pType->BounceOnWater) - return true; - - --this->BounceTimes; - this->ShouldBounce = false; - - // Calculate the velocity vector after bouncing - const auto groundNormalVector = this->GetGroundNormalVector(pBullet, pCell); - pBullet->Velocity = (this->LastVelocity - groundNormalVector * (this->LastVelocity * groundNormalVector) * 2) * pType->BounceCoefficient; - - // Detonate an additional warhead when bouncing? - if (pType->BounceDetonate) - { - const auto pFirer = pBullet->Owner; - const auto pOwner = pFirer ? pFirer->Owner : BulletExt::ExtMap.Find(pBullet)->FirerHouse; - WarheadTypeExt::DetonateAt(pBullet->WH, pBullet->Location, pFirer, pBullet->Health, pOwner); - } - - // Calculate the attenuation damage after bouncing - if (const int damage = pBullet->Health) - { - if (const int newDamage = static_cast(damage * pType->BounceAttenuation)) - pBullet->Health = newDamage; - else - pBullet->Health = damage > 0 ? 1 : -1; - } - - return false; -} - -BulletVelocity ParabolaTrajectory::GetGroundNormalVector(BulletClass* pBullet, CellClass* pCell) -{ - if (const auto index = pCell->SlopeIndex) - { - Vector2D factor { 0.0, 0.0 }; - - // 0.3763770469559380854890894443664 -> Unsorted::LevelHeight / sqrt(Unsorted::LevelHeight * Unsorted::LevelHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell) - // 0.9264665771223091335116047861327 -> Unsorted::LeptonsPerCell / sqrt(Unsorted::LevelHeight * Unsorted::LevelHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell) - // 0.3522530794922131411764879370407 -> Unsorted::LevelHeight / sqrt(2 * Unsorted::LevelHeight * Unsorted::LevelHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell) - // 0.8670845033654477321267395373309 -> Unsorted::LeptonsPerCell / sqrt(2 * Unsorted::LevelHeight * Unsorted::LevelHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell) - // 0.5333964609104418418483761938761 -> Unsorted::CellHeight / sqrt(2 * Unsorted::CellHeight * Unsorted::CellHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell) - // 0.6564879518897745745826168540013 -> Unsorted::LeptonsPerCell / sqrt(2 * Unsorted::CellHeight * Unsorted::CellHeight + Unsorted::LeptonsPerCell * Unsorted::LeptonsPerCell) - if (index <= 4) - factor = Vector2D{ 0.3763770469559380854890894443664, 0.9264665771223091335116047861327 }; - else if (index <= 12) - factor = Vector2D{ 0.3522530794922131411764879370407, 0.8670845033654477321267395373309 }; - else - factor = Vector2D{ 0.5333964609104418418483761938761, 0.6564879518897745745826168540013 }; - - 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 }; - } - } - - // 362.1 -> Unsorted::LeptonsPerCell * sqrt(2) - const auto horizontalVelocity = Vector2D{ pBullet->Velocity.X, pBullet->Velocity.Y }.Magnitude(); - const auto velocity = horizontalVelocity > 362.1 ? pBullet->Velocity * (362.1 / horizontalVelocity) : pBullet->Velocity; - const CoordStruct velocityCoords { static_cast(velocity.X), static_cast(velocity.Y), static_cast(velocity.Z) }; - - const auto cellHeight = pCell->Level * Unsorted::LevelHeight; - const auto bulletHeight = pBullet->Location.Z; - const auto lastCellHeight = MapClass::Instance.GetCellFloorHeight(pBullet->Location - velocityCoords); - - // 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 auto reverseSgnX = static_cast(pBullet->Velocity.X > 0.0 ? -1 : 1); - const auto reverseSgnY = static_cast(pBullet->Velocity.Y > 0.0 ? -1 : 1); - int index = 0; - - 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 }; - - index = 2; - } - } - else - { - if (this->CheckBulletHitCliff(cell.X + reverseSgnX, cell.Y - reverseSgnY, bulletHeight, lastCellHeight)) - { - if (this->CheckBulletHitCliff(cell.X, cell.Y + reverseSgnY, bulletHeight, lastCellHeight)) - index = 1; - else if (!this->CheckBulletHitCliff(cell.X - reverseSgnX, cell.Y + reverseSgnY, bulletHeight, lastCellHeight)) - index = 2; - } - 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)) - index = 1; - } - } - - // 0.4472135954999579392818347337463 -> 1 / sqrt(5) - // 0.8944271909999158785636694674925 -> 2 / sqrt(5) - if (index == 1) - return BulletVelocity{ 0.8944271909999158785636694674925 * reverseSgnX, 0.4472135954999579392818347337463 * reverseSgnY, 0.0 }; - else if (index == 2) - return BulletVelocity{ 0.4472135954999579392818347337463 * reverseSgnX, 0.8944271909999158785636694674925 * reverseSgnY, 0.0 }; - - // 0.7071067811865475244008443621049 -> 1 / sqrt(2) - return BulletVelocity{ 0.7071067811865475244008443621049 * reverseSgnX, 0.7071067811865475244008443621049 * reverseSgnY, 0.0 }; - } - - // Just ordinary ground - return BulletVelocity{ 0.0, 0.0, 1.0 }; -} - -bool ParabolaTrajectory::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; -} - -bool ParabolaTrajectory::BulletDetonatePreCheck(BulletClass* pBullet) -{ - if (this->ShouldDetonate) - return true; - - const auto pType = this->Type; - - // Check all conditions for premature detonation - if (pType->DetonationHeight >= 0 && pBullet->Velocity.Z < 1e-10 && (pBullet->Location.Z - pBullet->SourceCoords.Z) < pType->DetonationHeight) - return true; - - if (std::abs(pType->DetonationAngle) < 1e-10) - { - if (pBullet->Velocity.Z < 1e-10) - return true; - } - else if (std::abs(pType->DetonationAngle) < 90.0) - { - const auto horizontalVelocity = Vector2D{ pBullet->Velocity.X, pBullet->Velocity.Y }.Magnitude(); - - if (horizontalVelocity > 1e-10) - { - if ((pBullet->Velocity.Z / horizontalVelocity) < Math::tan(pType->DetonationAngle * Math::Pi / 180.0)) - return true; - } - else if (pType->DetonationAngle > 1e-10 || pBullet->Velocity.Z < 1e-10) - { - return true; - } - } - - return (pBullet->TargetCoords.DistanceFrom(pBullet->Location) < pType->DetonationDistance.Get()); -} - -bool ParabolaTrajectory::BulletDetonateLastCheck(BulletClass* pBullet, CellClass* pCell, double gravity, bool bounce) -{ - pBullet->Velocity.Z -= gravity; - - const CoordStruct velocityCoords { static_cast(pBullet->Velocity.X), static_cast(pBullet->Velocity.Y), static_cast(pBullet->Velocity.Z) }; - const auto futureCoords = pBullet->Location + velocityCoords; - - // Check all the cells that the next frame passes through like Straight - if (this->NeedExtraCheck) - { - const auto cellDist = CellClass::Coord2Cell(pBullet->Location) - CellClass::Coord2Cell(futureCoords); - const auto cellPace = CellStruct { static_cast(std::abs(cellDist.X)), static_cast(std::abs(cellDist.Y)) }; - const auto largePace = static_cast(std::max(cellPace.X, cellPace.Y)); - const auto stepCoord = largePace ? velocityCoords * (1.0 / largePace) : CoordStruct::Empty; - auto curCoord = pBullet->Location + stepCoord; - - for (size_t i = 1; i <= largePace; ++i) - { - // Below ground level? - const auto cellHeight = MapClass::Instance.GetCellFloorHeight(curCoord); - - if (curCoord.Z < cellHeight) - { - if (bounce) - return true; - - this->LastVelocity = pBullet->Velocity; - this->BulletDetonateEffectuate(pBullet, (static_cast(i - 0.5) / largePace)); - break; - } - - // Impact on the wall? - if (pBullet->Type->SubjectToWalls && pCell->OverlayTypeIndex != -1 && OverlayTypeClass::Array.GetItem(pCell->OverlayTypeIndex)->Wall) - { - pBullet->Velocity *= static_cast(i) / largePace; - this->ShouldDetonate = true; - return false; - } - - curCoord += stepCoord; - pCell = MapClass::Instance.GetCellAt(curCoord); - } - } - else - { - const auto cellHeight = MapClass::Instance.GetCellFloorHeight(futureCoords); - - if (cellHeight < futureCoords.Z) - return false; - - if (bounce) - return true; - - this->LastVelocity = pBullet->Velocity; - this->BulletDetonateEffectuate(pBullet, std::abs((pBullet->Location.Z - cellHeight) / pBullet->Velocity.Z)); - } - - return false; -} - -void ParabolaTrajectory::BulletDetonateEffectuate(BulletClass* pBullet, double velocityMult) -{ - if (velocityMult < 1.0) - pBullet->Velocity *= velocityMult; - - // Is it detonating or bouncing? - if (this->BounceTimes > 0) - this->ShouldBounce = true; - else - this->ShouldDetonate = true; -} diff --git a/src/Ext/Bullet/Trajectories/ParabolaTrajectory.h b/src/Ext/Bullet/Trajectories/ParabolaTrajectory.h deleted file mode 100644 index fa77157981..0000000000 --- a/src/Ext/Bullet/Trajectories/ParabolaTrajectory.h +++ /dev/null @@ -1,135 +0,0 @@ -#pragma once - -#include "PhobosTrajectory.h" - -enum class ParabolaFireMode -{ - Speed = 0, - Height = 1, - Angle = 2, - SpeedAndHeight = 3, - HeightAndAngle = 4, - SpeedAndAngle = 5, -}; - -class ParabolaTrajectoryType final : public PhobosTrajectoryType -{ -public: - ParabolaTrajectoryType() : PhobosTrajectoryType() - , DetonationDistance { Leptons(102) } - , TargetSnapDistance { Leptons(128) } - , OpenFireMode { ParabolaFireMode::Speed } - , ThrowHeight { 600 } - , LaunchAngle { 30.0 } - , LeadTimeCalculate { false } - , DetonationAngle { -90.0 } - , DetonationHeight { -1 } - , BounceTimes { 0 } - , BounceOnWater { false } - , BounceDetonate { false } - , BounceAttenuation { 0.8 } - , BounceCoefficient { 0.8 } - , OffsetCoord { { 0, 0, 0 } } - , RotateCoord { 0 } - , MirrorCoord { true } - , UseDisperseBurst { false } - , AxisOfRotation { { 0, 0, 1 } } - { } - - 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::Parabola; } - - Valueable DetonationDistance; - Valueable TargetSnapDistance; - Valueable OpenFireMode; - Valueable ThrowHeight; - Valueable LaunchAngle; - Valueable LeadTimeCalculate; - Valueable DetonationAngle; - Valueable DetonationHeight; - Valueable BounceTimes; - Valueable BounceOnWater; - Valueable BounceDetonate; - Valueable BounceAttenuation; - Valueable BounceCoefficient; - Valueable OffsetCoord; - Valueable RotateCoord; - Valueable MirrorCoord; - Valueable UseDisperseBurst; - Valueable AxisOfRotation; - -private: - template - void Serialize(T& Stm); -}; - -class ParabolaTrajectory final : public PhobosTrajectory -{ -public: - ParabolaTrajectory(noinit_t) { } - - ParabolaTrajectory(ParabolaTrajectoryType const* trajType) : Type { trajType } - , ThrowHeight { trajType->ThrowHeight > 0 ? trajType->ThrowHeight : 600 } - , BounceTimes { trajType->BounceTimes } - , OffsetCoord { trajType->OffsetCoord.Get() } - , UseDisperseBurst { trajType->UseDisperseBurst } - , ShouldDetonate { false } - , ShouldBounce { false } - , NeedExtraCheck { false } - , LastTargetCoord {} - , CurrentBurst { 0 } - , CountOfBurst { 0 } - , WaitOneFrame { 0 } - , 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(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) override; - virtual bool OnAI(BulletClass* pBullet) override; - virtual void OnAIPreDetonate(BulletClass* pBullet) override; - virtual void OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) override; - virtual TrajectoryCheckReturnType OnAITargetCoordCheck(BulletClass* pBullet) override; - virtual TrajectoryCheckReturnType OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) override; - - const ParabolaTrajectoryType* Type; - int ThrowHeight; - int BounceTimes; - CoordStruct OffsetCoord; - bool UseDisperseBurst; - bool ShouldDetonate; - bool ShouldBounce; - bool NeedExtraCheck; - CoordStruct LastTargetCoord; - int CurrentBurst; - int CountOfBurst; - int WaitOneFrame; - BulletVelocity LastVelocity; - -private: - template - void Serialize(T& Stm); - - void PrepareForOpenFire(BulletClass* pBullet); - bool BulletPrepareCheck(BulletClass* pBullet); - void CalculateBulletVelocityRightNow(BulletClass* pBullet, CoordStruct* pSourceCoords, double gravity); - void CalculateBulletVelocityLeadTime(BulletClass* pBullet, CoordStruct* pSourceCoords, double gravity); - void CheckIfNeedExtraCheck(BulletClass* pBullet); - double SearchVelocity(double horizontalDistance, int distanceCoordsZ, double radian, double gravity); - double CheckVelocityEquation(double horizontalDistance, int distanceCoordsZ, double velocity, double radian, double gravity); - double SolveFixedSpeedMeetTime(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double horizontalSpeed); - double SearchFixedHeightMeetTime(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double gravity); - double CheckFixedHeightEquation(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double meetTime, double gravity); - double SearchFixedAngleMeetTime(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double radian, double gravity); - double CheckFixedAngleEquation(CoordStruct* pSourceCrd, CoordStruct* pTargetCrd, CoordStruct* pOffsetCrd, double meetTime, double radian, double gravity); - bool CalculateBulletVelocityAfterBounce(BulletClass* pBullet, CellClass* pCell); - BulletVelocity GetGroundNormalVector(BulletClass* pBullet, CellClass* pCell); - bool CheckBulletHitCliff(short X, short Y, int bulletHeight, int lastCellHeight); - bool BulletDetonatePreCheck(BulletClass* pBullet); - bool BulletDetonateLastCheck(BulletClass* pBullet, CellClass* pCell, double gravity, bool bounce); - void BulletDetonateEffectuate(BulletClass* pBullet, double velocityMult); -}; diff --git a/src/Ext/Bullet/Trajectories/PhobosActualTrajectory.cpp b/src/Ext/Bullet/Trajectories/PhobosActualTrajectory.cpp new file mode 100644 index 0000000000..3fda8e2eac --- /dev/null +++ b/src/Ext/Bullet/Trajectories/PhobosActualTrajectory.cpp @@ -0,0 +1,182 @@ +#include "PhobosActualTrajectory.h" + +#include + +template +void ActualTrajectoryType::Serialize(T& Stm) +{ + Stm + .Process(this->RotateCoord) + .Process(this->OffsetCoord) + .Process(this->AxisOfRotation) + .Process(this->LeadTimeMaximum) + .Process(this->LeadTimeCalculate) + .Process(this->SubjectToGround) + .Process(this->EarlyDetonation) + .Process(this->DetonationHeight) + .Process(this->DetonationDistance) + .Process(this->TargetSnapDistance) + ; +} + +bool ActualTrajectoryType::Load(PhobosStreamReader& Stm, bool RegisterForChange) +{ + this->PhobosTrajectoryType::Load(Stm, false); + this->Serialize(Stm); + return true; +} + +bool ActualTrajectoryType::Save(PhobosStreamWriter& Stm) const +{ + this->PhobosTrajectoryType::Save(Stm); + const_cast(this)->Serialize(Stm); + return true; +} +/* +void ActualTrajectoryType::Read(CCINIClass* const pINI, const char* pSection) // Read separately +{ + this->PhobosTrajectoryType::Read(pINI, pSection); + INI_EX exINI(pINI); + + 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"); +} +*/ +template +void ActualTrajectory::Serialize(T& Stm) +{ + Stm + .Process(this->LastTargetCoord) + .Process(this->WaitStatus) + ; +} + +bool ActualTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange) +{ + this->PhobosTrajectory::Load(Stm, false); + this->Serialize(Stm); + return true; +} + +bool ActualTrajectory::Save(PhobosStreamWriter& Stm) const +{ + this->PhobosTrajectory::Save(Stm); + const_cast(this)->Serialize(Stm); + return true; +} + +void ActualTrajectory::OnUnlimbo() +{ + this->PhobosTrajectory::OnUnlimbo(); + + // Actual + const auto pBullet = this->Bullet; + const auto pBulletExt = BulletExt::ExtMap.Find(pBullet); + const auto pBulletTypeExt = pBulletExt->TypeExtData; + this->LastTargetCoord = pBullet->TargetCoords; + + // Survival time + if (pBulletTypeExt->LifeDuration > 0) + pBulletExt->LifeDurationTimer.Start(pBulletTypeExt->LifeDuration); +} + +bool ActualTrajectory::OnEarlyUpdate() +{ + if (this->WaitStatus != TrajectoryWaitStatus::NowReady && this->BulletPrepareCheck()) + return false; + + // Check whether need to detonate first + if (this->PhobosTrajectory::OnEarlyUpdate()) + return true; + + // In the phase of playing PreImpactAnim + if (this->Bullet->SpawnNextAnim) + return false; + + // Restore ProjectileRange + this->CheckProjectileRange(); + + // Waiting for new location calculated + return false; +} + +void ActualTrajectory::OnPreDetonate() +{ + const auto targetSnapDistance = static_cast(this->GetType())->TargetSnapDistance.Get(); + + // Can snap to target? + if (targetSnapDistance > 0) + { + const auto pBullet = this->Bullet; + const auto pTarget = abstract_cast(pBullet->Target); + const auto coords = pTarget ? pTarget->GetCoords() : pBullet->TargetCoords; + + // Whether to snap to target? + if (coords.DistanceFrom(pBullet->Location) <= targetSnapDistance) + { + const auto pExt = BulletExt::ExtMap.Find(pBullet); + pExt->SnappedToTarget = true; + pBullet->SetLocation(coords); + } + } + + this->PhobosTrajectory::OnPreDetonate(); +} + +bool ActualTrajectory::BulletPrepareCheck() +{ + // The time between bullets' Unlimbo() and Update() is completely uncertain. + // Target will update location after techno 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->WaitStatus == TrajectoryWaitStatus::JustUnlimbo) + { + if (const auto pTarget = this->Bullet->Target) + { + this->LastTargetCoord = pTarget->GetCoords(); + this->WaitStatus = TrajectoryWaitStatus::NextFrame; + return true; + } + } + + // Confirm the launch of the trajectory + this->WaitStatus = TrajectoryWaitStatus::NowReady; + this->FireTrajectory(); + return false; +} + +CoordStruct ActualTrajectory::GetOnlyStableOffsetCoords(const double rotateRadian) +{ + const auto pType = static_cast(this->GetType()); + auto offsetCoord = pType->OffsetCoord.Get(); + + // Check if mirroring is required + if (pType->MirrorCoord && this->CurrentBurst < 0) + offsetCoord.Y = -offsetCoord.Y; + + // Rotate the angle and return + return BulletExt::Vector2Coord(BulletExt::HorizontalRotate(offsetCoord, rotateRadian)); +} + +CoordStruct ActualTrajectory::GetInaccurateTargetCoords(const CoordStruct& baseCoord, const double distance) +{ + const auto pBullet = this->Bullet; + const auto pWeapon = pBullet->WeaponType; + const auto pTypeExt = BulletTypeExt::ExtMap.Find(pBullet->Type); + + // Don't know whether the weapon is correctly set, if not, a fixed value of 10 will be used + const double offsetMult = distance / (pWeapon ? pWeapon->Range : (10.0 * Unsorted::LeptonsPerCell)); + const int offsetMin = static_cast(offsetMult * pTypeExt->BallisticScatter_Min.Get(Leptons(0))); + const int offsetMax = static_cast(offsetMult * pTypeExt->BallisticScatter_Max.Get(Leptons(RulesClass::Instance->BallisticScatter))); + const int offsetDistance = ScenarioClass::Instance->Random.RandomRanged(offsetMin, offsetMax); + + // Substitute to calculate random coordinates + return MapClass::GetRandomCoordsNear(baseCoord, offsetDistance, false); +} diff --git a/src/Ext/Bullet/Trajectories/PhobosActualTrajectory.h b/src/Ext/Bullet/Trajectories/PhobosActualTrajectory.h new file mode 100644 index 0000000000..e553efb2d1 --- /dev/null +++ b/src/Ext/Bullet/Trajectories/PhobosActualTrajectory.h @@ -0,0 +1,109 @@ +#pragma once + +#include "PhobosTrajectory.h" + +#include + +/* + Base class: Actual Trajectory + + - The trajectory itself is an attacking object + - Used to share the properties/functions + + - for: + - Straight + - Bombard + - Missile + - Parabola +*/ + +enum class TrajectoryWaitStatus : unsigned char +{ + NowReady = 0, + NextFrame = 1, + JustUnlimbo = 2, +}; + +class ActualTrajectoryType : public PhobosTrajectoryType +{ +public: + ActualTrajectoryType() : PhobosTrajectoryType() + , RotateCoord { 0 } + , OffsetCoord { { 0, 0, 0 } } + , AxisOfRotation { { 0, 0, 1 } } + , LeadTimeMaximum { 0 } + , LeadTimeCalculate {} + , SubjectToGround { false } + , EarlyDetonation { false } + , DetonationHeight { -1 } + , DetonationDistance { Leptons(102) } + , TargetSnapDistance { Leptons(128) } + { } + + Valueable RotateCoord; // The maximum rotation angle of the initial velocity vector on the axis of rotation + Valueable OffsetCoord; // Offset of target position, refers to the initial target position on Missile + Valueable AxisOfRotation; // RotateCoord's rotation axis + Valueable LeadTimeMaximum; // Maximum prediction time + Nullable LeadTimeCalculate; // Predict the moving direction of the target + bool SubjectToGround; // Auto set + Valueable EarlyDetonation; // Calculating DetonationHeight in the rising phase rather than the falling phase + Valueable DetonationHeight; // At what height did it detonate in advance + Valueable DetonationDistance; // Explode at a distance from the target, different on AAA and BBB + Valueable TargetSnapDistance; // Snap to target when detonating with a distance less than this + + virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override; + virtual bool Save(PhobosStreamWriter& Stm) const override; +// virtual void Read(CCINIClass* const pINI, const char* pSection) override; // Read separately + +private: + template + void Serialize(T& Stm); +}; + +class ActualTrajectory : public PhobosTrajectory +{ +public: + ActualTrajectory() { } + ActualTrajectory(ActualTrajectoryType const* pTrajType, BulletClass* pBullet) + : PhobosTrajectory(pTrajType, pBullet) + , LastTargetCoord { CoordStruct::Empty } + , WaitStatus { TrajectoryWaitStatus::NowReady } + { } + + // TODO If we could calculate this before firing, perhaps it can solve the problem of one frame delay and not so correct turret orientation. + CoordStruct LastTargetCoord; // The target is located in the previous frame, used to calculate the lead time + TrajectoryWaitStatus WaitStatus; // Attempts to launch when update + + virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override; + virtual bool Save(PhobosStreamWriter& Stm) const override; + virtual void OnUnlimbo() override; + virtual bool OnEarlyUpdate() override; + virtual void OnPreDetonate() override; + virtual void FireTrajectory() { this->OpenFire(); } // New + + inline void CheckProjectileRange() + { + if (this->GetType()->Ranged) + { + const auto pBullet = this->Bullet; + pBullet->Range -= Game::F2I(this->MovingSpeed); + + if (pBullet->Range <= 0) + BulletExt::ExtMap.Find(pBullet)->Status |= TrajectoryStatus::Detonate; + } + } + inline double GetLeadTime(const double defaultTime) + { + const double maximum = static_cast(static_cast(this->GetType())->LeadTimeMaximum.Get()); + + return (maximum > 0.0 && defaultTime > maximum) ? maximum : defaultTime; + } + + bool BulletPrepareCheck(); + CoordStruct GetOnlyStableOffsetCoords(const double rotateRadian); + CoordStruct GetInaccurateTargetCoords(const CoordStruct& baseCoord, const double distance); + +private: + template + void Serialize(T& Stm); +}; diff --git a/src/Ext/Bullet/Trajectories/PhobosTrajectory.cpp b/src/Ext/Bullet/Trajectories/PhobosTrajectory.cpp index 2ec47db210..05f93f86ea 100644 --- a/src/Ext/Bullet/Trajectories/PhobosTrajectory.cpp +++ b/src/Ext/Bullet/Trajectories/PhobosTrajectory.cpp @@ -1,13 +1,12 @@ -#include -#include -#include +#include "PhobosTrajectory.h" +#include "ActualTrajectories/StraightTrajectory.h" +#include "ActualTrajectories/BombardTrajectory.h" +#include "ActualTrajectories/ParabolaTrajectory.h" -#include -#include +#include -#include "StraightTrajectory.h" -#include "BombardTrajectory.h" -#include "ParabolaTrajectory.h" +#include +#include TrajectoryTypePointer::TrajectoryTypePointer(TrajectoryFlag flag) { @@ -52,6 +51,35 @@ namespace detail return false; } + + template <> + inline bool read(TrajectoryFacing& value, INI_EX& parser, const char* pSection, const char* pKey) + { + if (parser.ReadString(pSection, pKey)) + { + static std::pair FlagNames[] = + { + {"Velocity", TrajectoryFacing::Velocity}, + {"Spin" ,TrajectoryFacing::Spin}, + {"Stable", TrajectoryFacing::Stable}, + {"Target" ,TrajectoryFacing::Target}, + {"Destination", TrajectoryFacing::Destination}, + {"FirerBody" ,TrajectoryFacing::FirerBody}, + {"FirerTurret", TrajectoryFacing::FirerTurret}, + }; + for (auto [name, flag] : FlagNames) + { + if (_strcmpi(parser.value(), name) == 0) + { + value = flag; + return true; + } + } + Debug::INIParseFailed(pSection, pKey, parser.value(), "Expected a new trajectory facing type"); + } + + return false; + } } void TrajectoryTypePointer::LoadFromINI(CCINIClass* pINI, const char* pSection) @@ -65,11 +93,7 @@ void TrajectoryTypePointer::LoadFromINI(CCINIClass* pINI, const char* pSection) std::construct_at(this, flag.Get()); } if (_ptr) - { - _ptr->Trajectory_Speed.Read(exINI, pSection, "Trajectory.Speed"); - _ptr->Trajectory_Speed = Math::max(0.001,_ptr->Trajectory_Speed); _ptr->Read(pINI, pSection); - } } bool TrajectoryTypePointer::Load(PhobosStreamReader& Stm, bool RegisterForChange) @@ -147,430 +171,725 @@ bool TrajectoryPointer::Save(PhobosStreamWriter& Stm) const // ------------------------------------------------------------------------------ // -// A rectangular shape with a custom width from the current frame to the next frame in length. -std::vector PhobosTrajectoryType::GetCellsInProximityRadius(BulletClass* pBullet, Leptons trajectoryProximityRange) +// Have generated projectile on the map and prepare for launch +void PhobosTrajectory::OnUnlimbo() { - // Seems like the y-axis is reversed, but it's okay. - const CoordStruct walkCoord { static_cast(pBullet->Velocity.X), static_cast(pBullet->Velocity.Y), 0 }; - const auto sideMult = trajectoryProximityRange / walkCoord.Magnitude(); + const auto pBullet = this->Bullet; + + // Due to various ways of firing weapons, the true firer may have already died + if (const auto pFirer = pBullet->Owner) + { + const auto burst = pFirer->CurrentBurstIndex; + this->CurrentBurst = (burst & 1) ? (-burst - 1) : burst; + } +} - 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 }; - const auto thisCell = CellClass::Coord2Cell(pBullet->Location); +// Something that needs to be done before updating the velocity of the projectile +bool PhobosTrajectory::OnEarlyUpdate() +{ + const auto pBullet = this->Bullet; + const auto pBulletExt = BulletExt::ExtMap.Find(pBullet); - auto cor1Cell = CellClass::Coord2Cell((pBullet->Location + cor1Coord)); - auto cor4Cell = CellClass::Coord2Cell((pBullet->Location + cor4Coord)); + // Update group index for members by themselves + if (pBulletExt->TrajectoryGroup) + pBulletExt->UpdateGroupIndex(); - const auto off1Cell = cor1Cell - thisCell; - const auto off4Cell = cor4Cell - thisCell; - const auto nextCell = CellClass::Coord2Cell((pBullet->Location + walkCoord)); + // In the phase of playing PreImpactAnim + if (pBullet->SpawnNextAnim) + return false; - auto cor2Cell = nextCell + off1Cell; - auto cor3Cell = nextCell + off4Cell; + // The previous check requires detonation at this time + if (pBulletExt->Status & (TrajectoryStatus::Detonate | TrajectoryStatus::Vanish)) + return true; - // Arrange the vertices of the rectangle in order from bottom to top. - int cornerIndex = 0; - CellStruct corner[4] = { cor1Cell, cor2Cell, cor3Cell, cor4Cell }; + // Check the remaining existence time + if (pBulletExt->LifeDurationTimer.Completed()) + return true; - for (int i = 1; i < 4; ++i) - { - if (corner[cornerIndex].Y > corner[i].Y) - cornerIndex = i; - } + // After the new target is confirmed, check if the tolerance time has ended + if (pBulletExt->CheckNoTargetLifeTime()) + return true; - cor1Cell = corner[cornerIndex]; - ++cornerIndex %= 4; - cor2Cell = corner[cornerIndex]; - ++cornerIndex %= 4; - cor3Cell = corner[cornerIndex]; - ++cornerIndex %= 4; - cor4Cell = corner[cornerIndex]; + // Based on the new target location, check how to change bullet velocity + if (this->OnVelocityCheck()) + return true; - std::vector recCells = PhobosTrajectoryType::GetCellsInRectangle(cor1Cell, cor4Cell, cor2Cell, cor3Cell); - std::vector recCellClass; - recCellClass.reserve(recCells.size()); + // Based on the new target location, rotate the bullet orientation + this->OnFacingUpdate(); - for (const auto& pCells : recCells) - { - if (CellClass* pRecCell = MapClass::Instance.TryGetCellAt(pCells)) - recCellClass.push_back(pRecCell); - } + // Fire weapons or warheads after the new velocity update is completed, ensure that it will not attack the wrong location + if (pBulletExt->FireAdditionals()) + return true; - return recCellClass; + // Detonate extra warhead on the obstacle after the pass through check is completed + pBulletExt->DetonateOnObstacle(); + return false; } -// Can ONLY fill RECTANGLE. Record cells in the order of "draw left boundary, draw right boundary, fill middle, and move up one level". -std::vector PhobosTrajectoryType::GetCellsInRectangle(CellStruct bottomStaCell, CellStruct leftMidCell, CellStruct rightMidCell, CellStruct topEndCell) +// It is possible to detonate here in advance or decide how to change the speed +bool PhobosTrajectory::OnVelocityCheck() { - std::vector recCells; - const auto cellNums = (std::abs(topEndCell.Y - bottomStaCell.Y) + 1) * (std::abs(rightMidCell.X - leftMidCell.X) + 1); - recCells.reserve(cellNums); - recCells.emplace_back(bottomStaCell); + const auto pBullet = this->Bullet; + const auto pBulletExt = BulletExt::ExtMap.Find(pBullet); + double ratio = 1.0; - 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) }; - auto mTheCurN = static_cast((middleThePace.Y - middleThePace.X) / 2.0); + // If there is an obstacle on the route, the bullet should need to reduce its speed so it will not penetrate the obstacle. + const auto pBulletTypeExt = pBulletExt->TypeExtData; + const bool checkThrough = (!pBulletTypeExt->ThroughBuilding || !pBulletTypeExt->ThroughVehicles); + const auto velocity = BulletExt::Get2DVelocity(this->MovingVelocity); - while (middleCurCell != topEndCell) + // Low speed with checkSubject was already done well + if (velocity < Unsorted::LeptonsPerCell) + { + // Blocked by obstacles? + if (checkThrough) { - if (mTheCurN > 0) - { - mTheCurN -= middleThePace.X; - middleCurCell.Y += middleTheUnit.Y; - recCells.emplace_back(middleCurCell); - } - else if (mTheCurN < 0) + 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)) { - mTheCurN += middleThePace.Y; - middleCurCell.X += middleTheUnit.X; - recCells.emplace_back(middleCurCell); + if (velocity > PhobosTrajectory::LowSpeedOffset) + ratio = (PhobosTrajectory::LowSpeedOffset / velocity); } - else + } + + // Check whether about to fall into the ground + if (std::abs(this->MovingVelocity.Z) > Unsorted::CellHeight && this->GetCanHitGround()) + { + const auto theTargetCoords = pBullet->Location + BulletExt::Vector2Coord(this->MovingVelocity); + const auto 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) { - mTheCurN += middleThePace.Y - middleThePace.X; - middleCurCell.X += middleTheUnit.X; - recCells.emplace_back(middleCurCell); - middleCurCell.X -= middleTheUnit.X; - middleCurCell.Y += middleTheUnit.Y; - recCells.emplace_back(middleCurCell); - middleCurCell.X += middleTheUnit.X; - recCells.emplace_back(middleCurCell); + // How much reduction is needed to calculate the velocity vector + const auto 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; } } } - else // Complete rectangle + else { - 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) }; - auto left1stCurN = static_cast((left1stPace.Y - left1stPace.X) / 2.0); - - 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) }; - auto left2ndCurN = static_cast((left2ndPace.Y - left2ndPace.X) / 2.0); - - 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) }; - auto right1stCurN = static_cast((right1stPace.Y - right1stPace.X) / 2.0); - - 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) }; - auto right2ndCurN = static_cast((right2ndPace.Y - right2ndPace.X) / 2.0); - - while (leftCurCell != topEndCell || rightCurCell != topEndCell) + // When in high speed, it's necessary to check each cell on the path that the next frame will pass through + const bool subjectToGround = this->GetCanHitGround(); + const auto pBulletType = pBullet->Type; + const bool subjectToWCS = pBulletType->SubjectToWalls || pBulletType->SubjectToCliffs || pBulletTypeExt->SubjectToSolid; + const bool subjectToFirestorm = !pBulletType->IgnoresFirestorm; + const bool checkCoords = subjectToGround || checkThrough || subjectToWCS; + + // If no inspection is needed, just skip it + if (checkCoords || subjectToFirestorm) { - while (leftCurCell != topEndCell) // Left + double locationDistance = 0.0; + bool velocityCheck = false; + 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; + + // Skip when no inspection is needed + if (checkCoords) { - if (!leftNext) // Bottom Left Side - { - if (left1stCurN > 0) - { - left1stCurN -= left1stPace.X; - leftCurCell.Y += left1stUnit.Y; - - if (leftCurCell == leftMidCell) - { - leftNext = true; - } - else - { - recCells.emplace_back(leftCurCell); - break; - } - } - else - { - left1stCurN += left1stPace.Y; - leftCurCell.X += left1stUnit.X; - - if (leftCurCell == leftMidCell) - { - leftNext = true; - leftSkip = true; - } - } - } - else // Top Left Side + 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 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 + const 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 (left2ndCurN >= 0) + if ((subjectToGround && (curCoord.Z + 16) < MapClass::Instance.GetCellFloorHeight(curCoord)) // Below ground level? (16 -> error range) + || (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)))) { - if (leftSkip) - { - leftSkip = false; - left2ndCurN -= left2ndPace.X; - leftCurCell.Y += left2ndUnit.Y; - } - else - { - leftContinue = true; - break; - } + locationDistance = BulletExt::Get2DDistance(curCoord, theSourceCoords); + velocityCheck = true; + break; } - else - { - left2ndCurN += left2ndPace.Y; - leftCurCell.X += left2ndUnit.X; - } - } - if (leftCurCell != rightCurCell) // Avoid double counting cells. - recCells.emplace_back(leftCurCell); + // There are no obstacles, continue to check the next cell + curCoord += stepCoord; + pLastCell = pCurCell; + pCurCell = MapClass::Instance.GetCellAt(curCoord); + } } - while (rightCurCell != topEndCell) // Right + // Check whether ignore firestorm wall before searching + if (subjectToFirestorm) { - if (!rightNext) // Bottom Right Side - { - if (right1stCurN > 0) - { - right1stCurN -= right1stPace.X; - rightCurCell.Y += right1stUnit.Y; - - if (rightCurCell == rightMidCell) - { - rightNext = true; - } - else - { - recCells.emplace_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; - } - } + const auto fireStormCoords = MapClass::Instance.FindFirstFirestorm(theSourceCoords, theTargetCoords, pOwner); - if (rightCurCell != leftCurCell) // Avoid double counting cells. - recCells.emplace_back(rightCurCell); - } + // Not empty when firestorm wall exists + if (fireStormCoords != CoordStruct::Empty) + { + const auto distance = BulletExt::Get2DDistance(fireStormCoords, theSourceCoords); - middleCurCell = leftCurCell; - middleCurCell.X += 1; + // Only record when the ratio is smaller + if (!velocityCheck || distance < locationDistance) + locationDistance = distance; - while (middleCurCell.X < rightCurCell.X) // Center - { - recCells.emplace_back(middleCurCell); - middleCurCell.X += 1; + velocityCheck = true; + } } - if (leftContinue) // Continue Top Left Side + // Check if the bullet needs to slow down the speed + if (velocityCheck) { - leftContinue = false; - left2ndCurN -= left2ndPace.X; - leftCurCell.Y += left2ndUnit.Y; - recCells.emplace_back(leftCurCell); - } + // Let the distance slightly exceed + locationDistance += PhobosTrajectory::LowSpeedOffset; - if (rightContinue) // Continue Top Right Side - { - rightContinue = false; - right2ndCurN -= right2ndPace.X; - rightCurCell.Y += right2ndUnit.Y; - recCells.emplace_back(rightCurCell); + // It may not be necessary to compare them again, but still do so + if (locationDistance < velocity) + ratio = (locationDistance / velocity); } } } - return recCells; + // Check if the distance to the destination exceeds the speed limit + if (this->RemainingDistance < this->MovingSpeed) + { + const auto newRatio = this->RemainingDistance / this->MovingSpeed; + + // Only record when the ratio is smaller + if (ratio > newRatio) + ratio = newRatio; + } + + // Only when the speed is very low will there be situations where the conditions are not met + if (ratio < 1.0) + this->MultiplyBulletVelocity(ratio, true); + + // Anyway, wait until later before detonating + return false; +} + +// How does the velocity of the projectile change +void PhobosTrajectory::OnVelocityUpdate(BulletVelocity* pSpeed, BulletVelocity* pPosition) +{ + // Set true moving velocity + if (this->MovingSpeed >= 0.5) + *pSpeed = this->MovingVelocity; + else + *pSpeed = BulletVelocity::Empty; +} + +// Something that needs to be done after updating the velocity of the projectile +TrajectoryCheckReturnType PhobosTrajectory::OnDetonateUpdate(const CoordStruct& position) +{ + // Need to detonate at the next location + if (BulletExt::ExtMap.Find(this->Bullet)->Status & (TrajectoryStatus::Detonate | TrajectoryStatus::Vanish)) + return TrajectoryCheckReturnType::Detonate; + + // Below ground level? (16 -> error range) + if (this->GetCanHitGround() && MapClass::Instance.GetCellFloorHeight(position) >= (position.Z + 16)) + return TrajectoryCheckReturnType::Detonate; + + // Skip all vanilla checks + return TrajectoryCheckReturnType::SkipGameCheck; +} + +// Something that needs to be done before detonation (and PreImpactAnim) +void PhobosTrajectory::OnPreDetonate() +{ + const auto pBullet = this->Bullet; + const auto pBulletExt = BulletExt::ExtMap.Find(pBullet); + const auto pBulletTypeExt = pBulletExt->TypeExtData; + + // Set detonate coords + pBullet->Data.Location = pBullet->Location; + + if (!(pBulletExt->Status & TrajectoryStatus::Vanish)) + { + if (!pBulletTypeExt->PeacefulVanish.Get(pBulletTypeExt->ProximityImpact)) + { + // Calculate the current damage + pBullet->Health = pBulletExt->GetTrueDamage(pBullet->Health, true); + return; + } + + pBulletExt->Status |= TrajectoryStatus::Vanish; + } + + // To skip all extra effects, no damage, no anims... + pBullet->Health = 0; + pBullet->Limbo(); + pBullet->UnInit(); +} + +// Something that needs to be done when the projectile is actually launched +void PhobosTrajectory::OpenFire() +{ + const auto pBullet = this->Bullet; + const auto& source = pBullet->SourceCoords; + const auto& target = pBullet->TargetCoords; + + // There may be a frame that hasn't started updating yet but will be drawn on the screen + if (this->MovingVelocity != BulletVelocity::Empty) + pBullet->Velocity = this->MovingVelocity; + else // Lead time bug + pBullet->Velocity = BulletVelocity { static_cast(target.X - source.X), static_cast(target.Y - source.Y), 0 }; + + const auto pType = this->GetType(); + + // Restricted to rotation only on a horizontal plane + if (pType->BulletFacing == TrajectoryFacing::Spin || pType->BulletFacingOnPlane) + pBullet->Velocity.Z = 0; + + // When the speed is delicate, there is a problem with the vanilla processing at the starting position + if (BulletExt::Get2DVelocity(this->MovingVelocity) < Unsorted::LeptonsPerCell) + { + const auto pBulletType = pBullet->Type; + + if (pBulletType->SubjectToWalls || pBulletType->SubjectToCliffs || BulletTypeExt::ExtMap.Find(pBulletType)->SubjectToSolid) + { + const auto pSourceCell = MapClass::Instance.GetCellAt(source); + const auto pTargetCell = MapClass::Instance.GetCellAt(target); + const auto pFirer = pBullet->Owner; + const auto pOwner = pFirer ? pFirer->Owner : BulletExt::ExtMap.Find(pBullet)->FirerHouse; + + if (TrajectoryHelper::GetObstacle(pSourceCell, pTargetCell, pSourceCell, pBullet->Location, pBulletType, pOwner)) + BulletExt::ExtMap.Find(pBullet)->Status |= TrajectoryStatus::Detonate; + } + } +} + +// Something that needs to be done when changing the target of the projectile +void PhobosTrajectory::SetBulletNewTarget(AbstractClass* const pTarget) +{ + const auto pBullet = this->Bullet; + pBullet->Target = pTarget; + pBullet->TargetCoords = pTarget->GetCoords(); +} + +// Something that needs to be done when setting the new speed of the projectile +bool PhobosTrajectory::CalculateBulletVelocity(const double speed) +{ + const double velocityLength = this->MovingVelocity.Magnitude(); + + // Check if it is a zero vector + if (velocityLength < BulletExt::Epsilon) + return true; + + // Reset speed vector + this->MovingVelocity *= speed / velocityLength; + this->MovingSpeed = speed; + return false; +} + +// Something that needs to be done when Multipling the speed of the projectile +void PhobosTrajectory::MultiplyBulletVelocity(const double ratio, const bool shouldDetonate) +{ + // Reset speed vector + this->MovingVelocity *= ratio; + this->MovingSpeed = this->MovingSpeed * ratio; + + // The next frame needs to detonate itself + if (shouldDetonate) + BulletExt::ExtMap.Find(this->Bullet)->Status |= TrajectoryStatus::Detonate; +} + +/*! + Rotate one vector by a certain angle towards the direction of another vector. + + \param vector Vector that needs to be rotated. This function directly modifies this value. + \param aim The final direction vector that needs to be oriented. It doesn't need to be a standardized vector. + \param turningRadian The maximum radius that can rotate. Note that it must be a positive number. + + \returns No return value, result is vector. + + \author CrimRecya +*/ +void PhobosTrajectory::RotateVector(BulletVelocity& vector, const BulletVelocity& aim, const double turningRadian) +{ + const double baseFactor = sqrt(aim.MagnitudeSquared() * vector.MagnitudeSquared()); + + // Not valid vector + if (baseFactor <= BulletExt::Epsilon) + { + vector = aim; + return; + } + + // Try using the vector to calculate the included angle + const double dotProduct = (aim * vector); + + // Calculate the cosine of the angle when the conditions are suitable + const double cosTheta = dotProduct / baseFactor; + + // Ensure that the result range of cos is correct + const double radian = Math::acos(Math::clamp(cosTheta, -1.0, 1.0)); + + // When the angle is small, aim directly at the target + if (std::abs(radian) <= turningRadian) + { + vector = aim; + return; + } + + // Calculate the rotation axis + auto rotationAxis = aim.CrossProduct(vector); + + // The radian can rotate, input the correct direction + const double rotateRadian = (radian < 0 ? turningRadian : -turningRadian); + + // Substitute to calculate new velocity + PhobosTrajectory::RotateAboutTheAxis(vector, rotationAxis, rotateRadian); +} + +/*! + Rotate the vector around the axis of rotation by a fixed angle. + + \param vector Vector that needs to be rotated. This function directly modifies this value. + \param axis The vector of rotation axis. This operation will standardize it. + \param radian The angle of rotation, positive or negative determines its direction of rotation. + + \returns No return value, result is vector. + + \author CrimRecya +*/ +void PhobosTrajectory::RotateAboutTheAxis(BulletVelocity& vector, BulletVelocity& axis, const double radian) +{ + const auto axisLengthSquared = axis.MagnitudeSquared(); + + // Zero axis vector is not acceptable + if (axisLengthSquared < BulletExt::Epsilon) + return; + + // Standardize rotation axis + axis *= 1 / sqrt(axisLengthSquared); + + // Rotate around the axis of rotation + const auto cosRotate = Math::cos(radian); + + // Substitute the formula to calculate the new vector + vector = (vector * cosRotate) + (axis * ((1 - cosRotate) * (vector * axis))) + (axis.CrossProduct(vector) * Math::sin(radian)); +} + +// Update of projectile facing direction +void PhobosTrajectory::OnFacingUpdate() +{ + const auto pType = this->GetType(); + const auto facing = pType->BulletFacing; + + // Cannot rotate + if (facing == TrajectoryFacing::Stable) + return; + + const auto pBullet = this->Bullet; + constexpr double ratio = Math::TwoPi / 256; + + if (facing == TrajectoryFacing::Spin) + { + const auto radian = Math::atan2(pBullet->Velocity.Y, pBullet->Velocity.X) + (pType->BulletROT * ratio); + pBullet->Velocity.X = Math::cos(radian); + pBullet->Velocity.Y = Math::sin(radian); + pBullet->Velocity.Z = 0; + return; + } + + auto desiredFacing = BulletVelocity::Empty; + + if (facing == TrajectoryFacing::Velocity) + { + desiredFacing = this->MovingVelocity; + } + else if (facing == TrajectoryFacing::Target) + { + if (const auto pTarget = pBullet->Target) + desiredFacing = BulletExt::Coord2Vector(pTarget->GetCoords() - pBullet->Location); + else + desiredFacing = BulletExt::Coord2Vector(pBullet->TargetCoords - pBullet->Location); + } + else if (facing == TrajectoryFacing::Destination) + { + desiredFacing = BulletExt::Coord2Vector(pBullet->TargetCoords - pBullet->Location); + } + else if (const auto pFirer = pBullet->Owner) + { + const double radian = -(facing == TrajectoryFacing::FirerTurret ? pFirer->TurretFacing() : pFirer->PrimaryFacing.Current()).GetRadian<65536>(); + desiredFacing.X = Math::cos(radian); + desiredFacing.Y = Math::sin(radian); + desiredFacing.Z = 0; + } + else + { + return; + } + + if (pType->BulletROT <= 0) + { + pBullet->Velocity = desiredFacing; + pBullet->Velocity *= (1 / pBullet->Velocity.Magnitude()); + } + else + { + // Restricted to rotation only on a horizontal plane + if (pType->BulletFacingOnPlane) + { + pBullet->Velocity.Z = 0; + desiredFacing.Z = 0; + } + + // Calculate specifically only when the ROT is reasonable + PhobosTrajectory::RotateVector(pBullet->Velocity, desiredFacing, (std::abs(pType->BulletROT) * ratio)); + + // Standardizing + pBullet->Velocity *= (1 / pBullet->Velocity.Magnitude()); + } +} + +// ============================= +// load / save + +void PhobosTrajectoryType::Read(CCINIClass* const pINI, const char* pSection) +{ + INI_EX exINI(pINI); + + this->Speed.Read(exINI, pSection, "Trajectory.Speed"); + this->Speed = Math::max(0.001, this->Speed); + this->BulletROT.Read(exINI, pSection, "Trajectory.BulletROT"); + this->BulletFacing.Read(exINI, pSection, "Trajectory.BulletFacing"); + this->BulletFacingOnPlane.Read(exINI, pSection, "Trajectory.BulletFacingOnPlane"); + this->MirrorCoord.Read(exINI, pSection, "Trajectory.MirrorCoord"); } bool PhobosTrajectoryType::Load(PhobosStreamReader& Stm, bool RegisterForChange) { - Stm - .Process(this->Trajectory_Speed); + this->Serialize(Stm); return true; } bool PhobosTrajectoryType::Save(PhobosStreamWriter& Stm) const { - Stm - .Process(this->Trajectory_Speed); + const_cast(this)->Serialize(Stm); return true; } -DEFINE_HOOK(0x4666F7, BulletClass_AI_Trajectories, 0x6) +template +void PhobosTrajectoryType::Serialize(T& Stm) { - enum { Detonate = 0x467E53 }; - - GET(BulletClass*, pThis, EBP); - - auto const pExt = BulletExt::ExtMap.Find(pThis); - bool detonate = false; + Stm + .Process(this->Speed) + .Process(this->BulletROT) + .Process(this->BulletFacing) + .Process(this->BulletFacingOnPlane) + .Process(this->MirrorCoord) + .Process(this->Ranged) + ; +} - if (auto const pTraj = pExt->Trajectory.get()) - detonate = pTraj->OnAI(pThis); +bool PhobosTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange) +{ + this->Serialize(Stm); + return true; +} - if (detonate && !pThis->SpawnNextAnim) - return Detonate; +bool PhobosTrajectory::Save(PhobosStreamWriter& Stm) const +{ + const_cast(this)->Serialize(Stm); + return true; +} - return 0; +template +void PhobosTrajectory::Serialize(T& Stm) +{ + Stm + .Process(this->Bullet) + .Process(this->MovingVelocity) + .Process(this->MovingSpeed) + .Process(this->RemainingDistance) + .Process(this->CurrentBurst) + ; } -DEFINE_HOOK(0x467E53, BulletClass_AI_PreDetonation_Trajectories, 0x6) +// ============================= +// hooks + +DEFINE_HOOK(0x468B72, BulletClass_Unlimbo_Trajectories, 0x5) { - GET(BulletClass*, pThis, EBP); + GET(BulletClass* const, pThis, EBX); + + const auto pExt = BulletExt::ExtMap.Find(pThis); - auto const pExt = BulletExt::ExtMap.Find(pThis); + // Initialize before trajectory unlimbo + pExt->InitializeOnUnlimbo(); - if (auto const pTraj = pExt->Trajectory.get()) - pTraj->OnAIPreDetonate(pThis); + if (const auto pTrajType = pExt->TypeExtData->TrajectoryType.get()) + { + pExt->Trajectory = pTrajType->CreateInstance(pThis); + pExt->Trajectory->OnUnlimbo(); + } + else if (pExt->TypeExtData->LifeDuration > 0) + { + pExt->LifeDurationTimer.Start(pExt->TypeExtData->LifeDuration); + } return 0; } -DEFINE_HOOK(0x46745C, BulletClass_AI_Position_Trajectories, 0x7) +DEFINE_HOOK(0x46745C, BulletClass_Update_TrajectoriesVelocityUpdate, 0x7) { - GET(BulletClass*, pThis, EBP); + GET(BulletClass* const, pThis, EBP); LEA_STACK(BulletVelocity*, pSpeed, STACK_OFFSET(0x1AC, -0x11C)); LEA_STACK(BulletVelocity*, pPosition, STACK_OFFSET(0x1AC, -0x144)); - auto const pExt = BulletExt::ExtMap.Find(pThis); + const auto pExt = BulletExt::ExtMap.Find(pThis); - if (auto const pTraj = pExt->Trajectory.get()) - pTraj->OnAIVelocity(pThis, pSpeed, pPosition); - - // Trajectory can use Velocity only for turning Image's direction - // The true position in the next frame will be calculate after here - if (pExt->Trajectory && pExt->LaserTrails.size()) + if (const auto pTraj = pExt->Trajectory.get()) { - CoordStruct futureCoords + pTraj->OnVelocityUpdate(pSpeed, pPosition); + // Trajectory can use Velocity only for turning Image's direction + // The true position in the next frame will be calculate after here + if (pExt->LaserTrails.size()) { - static_cast(pSpeed->X + pPosition->X), - static_cast(pSpeed->Y + pPosition->Y), - static_cast(pSpeed->Z + pPosition->Z) - }; + const auto futureCoords = BulletExt::Vector2Coord(*pSpeed + *pPosition); - for (const auto& pTrail : pExt->LaserTrails) - { - if (!pTrail->LastLocation.isset()) - pTrail->LastLocation = pThis->Location; + for (const auto& pTrail : pExt->LaserTrails) + { + if (!pTrail->LastLocation.isset()) + pTrail->LastLocation = pThis->Location; - pTrail->Update(futureCoords); + pTrail->Update(futureCoords); + } } } return 0; } -DEFINE_HOOK(0x4677D3, BulletClass_AI_TargetCoordCheck_Trajectories, 0x5) +DEFINE_HOOK(0x467609, BulletClass_Update_TrajectoriesSkipResetHeight, 0x6) +{ + enum { SkipGameCode = 0x46777A }; + + GET(BulletClass* const, pThis, EBP); + + if (!BulletExt::ExtMap.Find(pThis)->Trajectory) + return 0; + + R->ECX(0); + return SkipGameCode; +} + +DEFINE_HOOK(0x4677D3, BulletClass_Update_TrajectoriesDetonateUpdate, 0x5) { - enum { SkipCheck = 0x4678F8, ContinueAfterCheck = 0x467879, Detonate = 0x467E53 }; + enum { SkipCheck = 0x467B7A, ContinueAfterCheck = 0x467879, Detonate = 0x467E53 }; - GET(BulletClass*, pThis, EBP); + GET(BulletClass* const, pThis, EBP); + REF_STACK(const CoordStruct, position, STACK_OFFSET(0x1AC, -0x188)); - auto const pExt = BulletExt::ExtMap.Find(pThis); + const auto pExt = BulletExt::ExtMap.Find(pThis); - if (auto const pTraj = pExt->Trajectory.get()) + if (const auto pTraj = pExt->Trajectory.get()) { - switch (pTraj->OnAITargetCoordCheck(pThis)) + switch (pTraj->OnDetonateUpdate(position)) { case TrajectoryCheckReturnType::SkipGameCheck: - return SkipCheck; - break; + return SkipCheck; // Skip all vanilla check case TrajectoryCheckReturnType::SatisfyGameCheck: - return ContinueAfterCheck; - break; + return ContinueAfterCheck; // Continue next vanilla check case TrajectoryCheckReturnType::Detonate: - return Detonate; - break; - default: - break; + pThis->SetLocation(position); + return Detonate; // Directly detonate + default: // TrajectoryCheckReturnType::ExecuteGameCheck + break; // Do vanilla check } } + else if (pExt->Status & (TrajectoryStatus::Detonate | TrajectoryStatus::Vanish)) + { + pThis->SetLocation(position); + return Detonate; + } return 0; } -DEFINE_HOOK(0x467927, BulletClass_AI_TechnoCheck_Trajectories, 0x5) +DEFINE_HOOK(0x467BAC, BulletClass_Update_TrajectoriesCheckObstacle, 0x6) { - enum { SkipCheck = 0x467A26, ContinueAfterCheck = 0x467514 }; + enum { SkipVanillaCheck = 0x467C0C }; - GET(BulletClass*, pThis, EBP); - GET(TechnoClass*, pTechno, ESI); + GET(BulletClass* const, pThis, EBP); - auto const pExt = BulletExt::ExtMap.Find(pThis); - - if (auto const pTraj = pExt->Trajectory.get()) + if (const auto pTraj = BulletExt::ExtMap.Find(pThis)->Trajectory.get()) { - switch (pTraj->OnAITechnoCheck(pThis, pTechno)) - { - case TrajectoryCheckReturnType::SkipGameCheck: - return SkipCheck; - break; - case TrajectoryCheckReturnType::SatisfyGameCheck: - return ContinueAfterCheck; - break; - default: - break; - } + // Already checked when the speed is high + if (BulletExt::Get2DVelocity(pTraj->MovingVelocity) >= Unsorted::LeptonsPerCell) + return SkipVanillaCheck; } return 0; } -DEFINE_HOOK(0x468B72, BulletClass_Unlimbo_Trajectories, 0x5) +DEFINE_HOOK(0x467E53, BulletClass_Update_TrajectoriesPreDetonation, 0x6) +{ + GET(BulletClass* const, pThis, EBP); + + const auto pExt = BulletExt::ExtMap.Find(pThis); + + if (const auto pTraj = pExt->Trajectory.get()) + pTraj->OnPreDetonate(); + else // Due to virtual call in trajectory and the different processing order of base class, this should be separated + pExt->CheckOnPreDetonate(); + + return 0; +} + +DEFINE_HOOK(0x468585, BulletClass_PointerExpired_Trajectories, 0x9) +{ + enum { SkipSetCellAsTarget = 0x46859C }; + + GET(BulletClass* const, pThis, ESI); + + return BulletExt::ExtMap.Find(pThis)->Trajectory ? SkipSetCellAsTarget : 0; +} + +// Vanilla inertia effect only for bullets with ROT=0 +DEFINE_HOOK(0x415F25, AircraftClass_Fire_TrajectorySkipInertiaEffect, 0x6) { - GET(BulletClass*, pThis, EBX); - GET_STACK(CoordStruct*, pCoord, STACK_OFFSET(0x54, 0x4)); - GET_STACK(BulletVelocity*, pVelocity, STACK_OFFSET(0x54, 0x8)); + enum { SkipCheck = 0x4160BC }; - auto const pExt = BulletExt::ExtMap.Find(pThis); - auto const pTypeExt = pExt->TypeExtData; + GET(BulletClass*, pThis, ESI); - if (pTypeExt->TrajectoryType) + if (BulletExt::ExtMap.Find(pThis)->Trajectory) + return SkipCheck; + + return 0; +} + +// Update trajectories target +DEFINE_HOOK(0x46B5A4, BulletClass_SetTarget_SetTrajectoryTarget, 0x6) +{ + enum { SkipGameCode = 0x46B5AA }; + + GET(BulletClass*, pThis, ECX); + GET(AbstractClass*, pTarget, EAX); + + if (const auto pTraj = BulletExt::ExtMap.Find(pThis)->Trajectory.get()) { - pExt->Trajectory = pTypeExt->TrajectoryType->CreateInstance(); - pExt->Trajectory->OnUnlimbo(pThis, pCoord, pVelocity); + if (pTarget) + pTraj->SetBulletNewTarget(pTarget); + else + pThis->Target = nullptr; + + return SkipGameCode; } return 0; diff --git a/src/Ext/Bullet/Trajectories/PhobosTrajectory.h b/src/Ext/Bullet/Trajectories/PhobosTrajectory.h index e92617bed9..93a42cd5d2 100644 --- a/src/Ext/Bullet/Trajectories/PhobosTrajectory.h +++ b/src/Ext/Bullet/Trajectories/PhobosTrajectory.h @@ -1,10 +1,12 @@ #pragma once +#include + +#include +#include #include #include -#include - enum class TrajectoryFlag : int { Invalid = -1, @@ -20,40 +22,104 @@ enum class TrajectoryCheckReturnType : int SatisfyGameCheck = 2, Detonate = 3 }; + +enum class TrajectoryFacing : unsigned char +{ + Velocity = 0, + Spin = 1, + Stable = 2, + Target = 3, + Destination = 4, + FirerBody = 5, + FirerTurret = 6 +}; + +enum class TrajectoryStatus : unsigned char +{ + None = 0x0, + Detonate = 0x1, + Vanish = 0x2, + Bounce = 0x4 +}; +MAKE_ENUM_FLAGS(TrajectoryStatus); + class PhobosTrajectory; class PhobosTrajectoryType { public: - PhobosTrajectoryType() { } + PhobosTrajectoryType() : + Speed { 100.0 } + , BulletROT { 0 } + , BulletFacing { TrajectoryFacing::Velocity } + , BulletFacingOnPlane { false } + , MirrorCoord { true } + , Ranged { false } + { } + + Valueable Speed; // The speed that a projectile should reach + Valueable BulletROT; // The rotational speed of the projectile image that does not affect the direction of movement + Valueable BulletFacing; // Image facing + Valueable BulletFacingOnPlane; // Image facing only on horizontal plane + Valueable MirrorCoord; // Should mirror offset + bool Ranged; // Auto set virtual ~PhobosTrajectoryType() noexcept = default; virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange); virtual bool Save(PhobosStreamWriter& Stm) const; - virtual TrajectoryFlag Flag() const = 0; - virtual void Read(CCINIClass* const pINI, const char* pSection) = 0; - [[nodiscard]] virtual std::unique_ptr CreateInstance() const = 0; + virtual TrajectoryFlag Flag() const { return TrajectoryFlag::Invalid; } + virtual void Read(CCINIClass* const pINI, const char* pSection); + [[nodiscard]] virtual std::unique_ptr CreateInstance(BulletClass* pBullet) const = 0; - static std::vector GetCellsInProximityRadius(BulletClass* pBullet, Leptons trajectoryProximityRange); private: - static std::vector GetCellsInRectangle(CellStruct bottomStaCell, CellStruct leftMidCell, CellStruct rightMidCell, CellStruct topEndCell); - -public: - Valueable Trajectory_Speed { 100.0 }; + template + void Serialize(T& Stm); }; class PhobosTrajectory { public: + static constexpr double LowSpeedOffset = 32.0; + + PhobosTrajectory() { } + PhobosTrajectory(PhobosTrajectoryType const* pTrajType, BulletClass* pBullet) : + Bullet { pBullet } + , MovingVelocity { BulletVelocity::Empty } + , MovingSpeed { 0 } + , RemainingDistance { 1 } + , CurrentBurst { 0 } + { } + + BulletClass* Bullet; // Bullet attached to + BulletVelocity MovingVelocity; // The vector used for calculating speed + double MovingSpeed; // The current speed value + int RemainingDistance; // Remaining distance from the self explosion location + int CurrentBurst; // Current burst index, mirror is required for negative numbers + virtual ~PhobosTrajectory() noexcept = default; - virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) = 0; - virtual bool Save(PhobosStreamWriter& Stm) const = 0; - virtual TrajectoryFlag Flag() const = 0; - virtual void OnUnlimbo(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) = 0; - virtual bool OnAI(BulletClass* pBullet) = 0; - virtual void OnAIPreDetonate(BulletClass* pBullet) = 0; - virtual void OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) = 0; - virtual TrajectoryCheckReturnType OnAITargetCoordCheck(BulletClass* pBullet) = 0; - virtual TrajectoryCheckReturnType OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) = 0; + virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange); + virtual bool Save(PhobosStreamWriter& Stm) const; + virtual TrajectoryFlag Flag() const { return TrajectoryFlag::Invalid; } + virtual void OnUnlimbo(); + virtual bool OnEarlyUpdate(); + virtual bool OnVelocityCheck(); + virtual void OnVelocityUpdate(BulletVelocity* pSpeed, BulletVelocity* pPosition); + virtual TrajectoryCheckReturnType OnDetonateUpdate(const CoordStruct& position); + virtual void OnPreDetonate(); + virtual const PhobosTrajectoryType* GetType() const = 0; + virtual void OpenFire(); + virtual bool GetCanHitGround() const { return true; } + virtual void SetBulletNewTarget(AbstractClass* const pTarget); + virtual bool CalculateBulletVelocity(const double speed); + virtual void MultiplyBulletVelocity(const double ratio, const bool shouldDetonate); + + static void RotateVector(BulletVelocity& vector, const BulletVelocity& aim, const double turningRadian); + static void RotateAboutTheAxis(BulletVelocity& vector, BulletVelocity& axis, const double radian); + + void OnFacingUpdate(); + +private: + template + void Serialize(T& Stm); }; /* diff --git a/src/Ext/Bullet/Trajectories/SampleTrajectory.cpp b/src/Ext/Bullet/Trajectories/SampleTrajectory.cpp index cd3a3e480d..1059b1b8b3 100644 --- a/src/Ext/Bullet/Trajectories/SampleTrajectory.cpp +++ b/src/Ext/Bullet/Trajectories/SampleTrajectory.cpp @@ -3,9 +3,9 @@ #include // Create -std::unique_ptr SampleTrajectoryType::CreateInstance() const +std::unique_ptr SampleTrajectoryType::CreateInstance(BulletClass* pBullet) const { - return std::make_unique(this); + return std::make_unique(this, pBullet); } // Save and Load for type @@ -34,8 +34,11 @@ bool SampleTrajectoryType::Save(PhobosStreamWriter& Stm) const // INI reading stuff void SampleTrajectoryType::Read(CCINIClass* const pINI, const char* pSection) { + this->PhobosTrajectoryType::Read(pINI, pSection); INI_EX exINI(pINI); - this->TargetSnapDistance.Read(exINI, pSection, "Trajectory.Sample.TargetSnapDistance"); + + // Sample + this->TargetSnapDistance.Read(exINI, pSection, "Trajectory.TargetSnapDistance"); } // Save and Load for entity @@ -44,81 +47,115 @@ void SampleTrajectory::Serialize(T& Stm) { Stm .Process(this->Type) - .Process(this->TargetSnapDistance) ; } bool SampleTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange) { + this->PhobosTrajectory::Load(Stm, false); this->Serialize(Stm); return true; } bool SampleTrajectory::Save(PhobosStreamWriter& Stm) const { + this->PhobosTrajectory::Save(Stm); const_cast(this)->Serialize(Stm); return true; } -// Do some math here to set the initial speed or location of your bullet. -// Be careful not to let the bullet speed too fast without other processing. -void SampleTrajectory::OnUnlimbo(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) +// Record some information for your bullet. +void SampleTrajectory::OnUnlimbo() { - pBullet->Velocity.X = static_cast(pBullet->TargetCoords.X - pBullet->SourceCoords.X); - pBullet->Velocity.Y = static_cast(pBullet->TargetCoords.Y - pBullet->SourceCoords.Y); - pBullet->Velocity.Z = static_cast(pBullet->TargetCoords.Z - pBullet->SourceCoords.Z); - pBullet->Velocity *= this->Type->Trajectory_Speed / pBullet->Velocity.Magnitude(); + this->PhobosTrajectory::OnUnlimbo(); + + // Sample + const auto pBullet = this->Bullet; + this->RemainingDistance += static_cast(pBullet->SourceCoords.DistanceFrom(pBullet->TargetCoords)); + + this->OpenFire(); } -// Some early checks here, returns whether or not to detonate the bullet. -// You can change the bullet's true velocity or set its location here. If you modify them here, it will affect the incoming parameters in OnAIVelocity. -bool SampleTrajectory::OnAI(BulletClass* pBullet) +// Some checks here, returns whether or not to detonate the bullet. +// You can change the bullet's true velocity or set its location here. If you modify them here, it will affect the incoming parameters in OnVelocityUpdate(). +bool SampleTrajectory::OnEarlyUpdate() { - const auto distance = pBullet->TargetCoords.DistanceFrom(pBullet->Location); - const auto velocity = pBullet->Velocity.Magnitude(); + return this->PhobosTrajectory::OnEarlyUpdate(); +} - if (distance < velocity) - pBullet->Velocity *= (distance / velocity); +// What needs to be done before launching the weapon after calculating the new speed. +bool SampleTrajectory::OnVelocityCheck() +{ + return this->PhobosTrajectory::OnVelocityCheck(); +} - return false; +// Where you can update the bullet's speed and position. But I would recommend that you complete the calculation at OnEarlyUpdate(). +// pSpeed: From the basic `Velocity` of the bullet plus gravity. It is only used in the calculation of this frame and will not be retained to the next frame. +// pPosition: From the current `Location` of the bullet, then the bullet will be set location to (*pSpeed + *pPosition). So don't use SetLocation here. +// You can also do additional processing here so that the position of the bullet will not change with its true velocity. +void SampleTrajectory::OnVelocityUpdate(BulletVelocity* pSpeed, BulletVelocity* pPosition) +{ + this->PhobosTrajectory::OnVelocityUpdate(pSpeed, pPosition); +} + +// Where additional checks based on bullet reaching its target coordinate can be done. +// Vanilla code will do additional checks regarding buildings on target coordinate and Vertical projectiles and will detonate the projectile if they pass. +// Return value determines what is done regards to the game checks: they can be skipped, executed as normal or treated as if the condition is already satisfied. +TrajectoryCheckReturnType SampleTrajectory::OnDetonateUpdate(const CoordStruct& position) +{ + if (this->PhobosTrajectory::OnDetonateUpdate(position) == TrajectoryCheckReturnType::Detonate) + return TrajectoryCheckReturnType::Detonate; + + this->RemainingDistance -= static_cast(this->MovingSpeed); + + if (this->RemainingDistance < 0) + return TrajectoryCheckReturnType::Detonate; + + return TrajectoryCheckReturnType::SkipGameCheck; } // At this time, the bullet has hit the target and is ready to detonate. // You can make it change before detonating. -void SampleTrajectory::OnAIPreDetonate(BulletClass* pBullet) +void SampleTrajectory::OnPreDetonate() { + const auto pBullet = this->Bullet; auto pTarget = abstract_cast(pBullet->Target); - auto pCoords = pTarget ? pTarget->GetCoords() : pBullet->Data.Location; + auto pCoords = pTarget ? pTarget->GetCoords() : pBullet->TargetCoords; - if (pCoords.DistanceFrom(pBullet->Location) <= this->TargetSnapDistance) + // Can snap to target? + if (pCoords.DistanceFrom(pBullet->Location) <= this->Type->TargetSnapDistance.Get()) { BulletExt::ExtMap.Find(pBullet)->SnappedToTarget = true; pBullet->SetLocation(pCoords); } + + this->PhobosTrajectory::OnPreDetonate(); } -// Where you can update the bullet's speed and position. -// pSpeed: From the basic `Velocity` of the bullet plus gravity. It is only used in the calculation of this frame and will not be retained to the next frame. -// pPosition: From the current `Location` of the bullet, then the bullet will be set location to (*pSpeed + *pPosition). So don't use SetLocation here. -// You can also do additional processing here so that the position of the bullet will not change with its true velocity. -void SampleTrajectory::OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) +// Do some math here to set the initial speed or location of your bullet. +// Be careful not to let the bullet speed too fast without other processing. +void SampleTrajectory::OpenFire() { - pSpeed->Z += BulletTypeExt::GetAdjustedGravity(pBullet->Type); + const auto pBullet = this->Bullet; + this->MovingVelocity = BulletExt::Coord2Vector(pBullet->TargetCoords - pBullet->SourceCoords); + this->CalculateBulletVelocity(this->Type->Speed); + this->PhobosTrajectory::OpenFire(); } -// Where additional checks based on bullet reaching its target coordinate can be done. -// Vanilla code will do additional checks regarding buildings on target coordinate and Vertical projectiles and will detonate the projectile if they pass. -// Return value determines what is done regards to the game checks: they can be skipped, executed as normal or treated as if the condition is already satisfied. -TrajectoryCheckReturnType SampleTrajectory::OnAITargetCoordCheck(BulletClass* pBullet) +// Does the projectile detonate when it lands below the ground +bool SampleTrajectory::GetCanHitGround() const { - return TrajectoryCheckReturnType::ExecuteGameCheck; // Execute game checks. + return true; } -// Where additional checks based on a TechnoClass instance in same cell as the bullet can be done. -// Vanilla code will do additional trajectory alterations here if there is an enemy techno in the cell. -// Return value determines what is done regards to the game checks: they can be skipped, executed as normal or treated as if the condition is already satisfied. -// pTechno: TechnoClass instance in same cell as the bullet. Note that you should first check whether it is a nullptr. -TrajectoryCheckReturnType SampleTrajectory::OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) +// How to calculate when inputting velocity values after updating the velocity vector each time +bool SampleTrajectory::CalculateBulletVelocity(const double speed) +{ + return this->PhobosTrajectory::CalculateBulletVelocity(speed); +}; + +// How to do when should change to a new target +void SampleTrajectory::SetBulletNewTarget(AbstractClass* const pTarget) { - return TrajectoryCheckReturnType::ExecuteGameCheck; // Execute game checks. + this->PhobosTrajectory::SetBulletNewTarget(pTarget); } diff --git a/src/Ext/Bullet/Trajectories/SampleTrajectory.h b/src/Ext/Bullet/Trajectories/SampleTrajectory.h index defb426c46..abb2ea97e0 100644 --- a/src/Ext/Bullet/Trajectories/SampleTrajectory.h +++ b/src/Ext/Bullet/Trajectories/SampleTrajectory.h @@ -9,14 +9,14 @@ class SampleTrajectoryType final : public PhobosTrajectoryType , TargetSnapDistance { Leptons(128) } { } + Valueable TargetSnapDistance; + virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override; virtual bool Save(PhobosStreamWriter& Stm) const override; - virtual std::unique_ptr CreateInstance() const override; + virtual std::unique_ptr CreateInstance(BulletClass* pBullet) const override; virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Invalid; } // TrajectoryFlag virtual void Read(CCINIClass* const pINI, const char* pSection) override; - Valueable TargetSnapDistance; - private: template void Serialize(T& Stm); @@ -26,23 +26,27 @@ class SampleTrajectory final : public PhobosTrajectory { public: SampleTrajectory(noinit_t) { } - - SampleTrajectory(SampleTrajectoryType const* trajType) : Type { trajType } - , TargetSnapDistance { trajType->TargetSnapDistance } + SampleTrajectory(SampleTrajectoryType const* pTrajType, BulletClass* pBullet) + : PhobosTrajectory(pTrajType, pBullet) + , Type { pTrajType } { } + SampleTrajectoryType const* Type; + virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override; virtual bool Save(PhobosStreamWriter& Stm) const override; virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Invalid; } // TrajectoryFlag - virtual void OnUnlimbo(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) override; - virtual bool OnAI(BulletClass* pBullet) override; - virtual void OnAIPreDetonate(BulletClass* pBullet) override; - virtual void OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) override; - virtual TrajectoryCheckReturnType OnAITargetCoordCheck(BulletClass* pBullet) override; - virtual TrajectoryCheckReturnType OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) override; - - SampleTrajectoryType const* Type; - Leptons TargetSnapDistance; + virtual void OnUnlimbo() override; + virtual bool OnEarlyUpdate() override; + virtual bool OnVelocityCheck() override; + virtual void OnVelocityUpdate(BulletVelocity* pSpeed, BulletVelocity* pPosition) 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 bool GetCanHitGround() const override; + virtual void SetBulletNewTarget(AbstractClass* const pTarget) override; + virtual bool CalculateBulletVelocity(const double speed) override; private: template diff --git a/src/Ext/Bullet/Trajectories/StraightTrajectory.cpp b/src/Ext/Bullet/Trajectories/StraightTrajectory.cpp deleted file mode 100644 index 20e488e05e..0000000000 --- a/src/Ext/Bullet/Trajectories/StraightTrajectory.cpp +++ /dev/null @@ -1,1058 +0,0 @@ -#include "StraightTrajectory.h" - -#include -#include -#include - -#include -#include -#include -#include - -std::unique_ptr StraightTrajectoryType::CreateInstance() const -{ - return std::make_unique(this); -} - -template -void StraightTrajectoryType::Serialize(T& Stm) -{ - Stm - .Process(this->DetonationDistance) - .Process(this->TargetSnapDistance) - .Process(this->ApplyRangeModifiers) - .Process(this->PassThrough) - .Process(this->PassDetonate) - .Process(this->PassDetonateWarhead) - .Process(this->PassDetonateDamage) - .Process(this->PassDetonateDelay) - .Process(this->PassDetonateInitialDelay) - .Process(this->PassDetonateLocal) - .Process(this->LeadTimeCalculate) - .Process(this->OffsetCoord) - .Process(this->RotateCoord) - .Process(this->MirrorCoord) - .Process(this->UseDisperseBurst) - .Process(this->AxisOfRotation) - .Process(this->ProximityImpact) - .Process(this->ProximityWarhead) - .Process(this->ProximityDamage) - .Process(this->ProximityRadius) - .Process(this->ProximityDirect) - .Process(this->ProximityMedial) - .Process(this->ProximityAllies) - .Process(this->ProximityFlight) - .Process(this->ThroughVehicles) - .Process(this->ThroughBuilding) - .Process(this->SubjectToGround) - .Process(this->ConfineAtHeight) - .Process(this->EdgeAttenuation) - .Process(this->CountAttenuation) - ; -} - -bool StraightTrajectoryType::Load(PhobosStreamReader& Stm, bool RegisterForChange) -{ - this->PhobosTrajectoryType::Load(Stm, false); - this->Serialize(Stm); - return true; -} - -bool StraightTrajectoryType::Save(PhobosStreamWriter& Stm) const -{ - this->PhobosTrajectoryType::Save(Stm); - const_cast(this)->Serialize(Stm); - return true; -} - -void StraightTrajectoryType::Read(CCINIClass* const pINI, const char* pSection) -{ - INI_EX exINI(pINI); - - this->ApplyRangeModifiers.Read(exINI, pSection, "Trajectory.Straight.ApplyRangeModifiers"); - this->DetonationDistance.Read(exINI, pSection, "Trajectory.Straight.DetonationDistance"); - this->TargetSnapDistance.Read(exINI, pSection, "Trajectory.Straight.TargetSnapDistance"); - this->PassThrough.Read(exINI, pSection, "Trajectory.Straight.PassThrough"); - this->PassDetonate.Read(exINI, pSection, "Trajectory.Straight.PassDetonate"); - this->PassDetonateWarhead.Read(exINI, pSection, "Trajectory.Straight.PassDetonateWarhead"); - this->PassDetonateDamage.Read(exINI, pSection, "Trajectory.Straight.PassDetonateDamage"); - this->PassDetonateDelay.Read(exINI, pSection, "Trajectory.Straight.PassDetonateDelay"); - this->PassDetonateInitialDelay.Read(exINI, pSection, "Trajectory.Straight.PassDetonateInitialDelay"); - this->PassDetonateLocal.Read(exINI, pSection, "Trajectory.Straight.PassDetonateLocal"); - this->LeadTimeCalculate.Read(exINI, pSection, "Trajectory.Straight.LeadTimeCalculate"); - this->OffsetCoord.Read(exINI, pSection, "Trajectory.Straight.OffsetCoord"); - this->RotateCoord.Read(exINI, pSection, "Trajectory.Straight.RotateCoord"); - this->MirrorCoord.Read(exINI, pSection, "Trajectory.Straight.MirrorCoord"); - this->UseDisperseBurst.Read(exINI, pSection, "Trajectory.Straight.UseDisperseBurst"); - this->AxisOfRotation.Read(exINI, pSection, "Trajectory.Straight.AxisOfRotation"); - this->ProximityImpact.Read(exINI, pSection, "Trajectory.Straight.ProximityImpact"); - this->ProximityWarhead.Read(exINI, pSection, "Trajectory.Straight.ProximityWarhead"); - this->ProximityDamage.Read(exINI, pSection, "Trajectory.Straight.ProximityDamage"); - this->ProximityRadius.Read(exINI, pSection, "Trajectory.Straight.ProximityRadius"); - this->ProximityDirect.Read(exINI, pSection, "Trajectory.Straight.ProximityDirect"); - this->ProximityMedial.Read(exINI, pSection, "Trajectory.Straight.ProximityMedial"); - this->ProximityAllies.Read(exINI, pSection, "Trajectory.Straight.ProximityAllies"); - this->ProximityFlight.Read(exINI, pSection, "Trajectory.Straight.ProximityFlight"); - this->ThroughVehicles.Read(exINI, pSection, "Trajectory.Straight.ThroughVehicles"); - this->ThroughBuilding.Read(exINI, pSection, "Trajectory.Straight.ThroughBuilding"); - this->SubjectToGround.Read(exINI, pSection, "Trajectory.Straight.SubjectToGround"); - this->ConfineAtHeight.Read(exINI, pSection, "Trajectory.Straight.ConfineAtHeight"); - this->EdgeAttenuation.Read(exINI, pSection, "Trajectory.Straight.EdgeAttenuation"); - this->EdgeAttenuation = Math::max(0.0, this->EdgeAttenuation); - this->CountAttenuation.Read(exINI, pSection, "Trajectory.Straight.CountAttenuation"); - this->CountAttenuation = Math::max(0.0, this->CountAttenuation); -} - -template -void StraightTrajectory::Serialize(T& Stm) -{ - Stm - .Process(this->Type) - .Process(this->DetonationDistance) - .Process(this->PassDetonateDamage) - .Process(this->PassDetonateTimer) - .Process(this->OffsetCoord) - .Process(this->UseDisperseBurst) - .Process(this->ProximityImpact) - .Process(this->ProximityDamage) - .Process(this->RemainingDistance) - .Process(this->ExtraCheck) - .Process(this->TheCasualty) - .Process(this->FirepowerMult) - .Process(this->AttenuationRange) - .Process(this->LastTargetCoord) - .Process(this->CurrentBurst) - .Process(this->CountOfBurst) - .Process(this->WaitOneFrame) - ; -} - -bool StraightTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange) -{ - this->Serialize(Stm); - return true; -} - -bool StraightTrajectory::Save(PhobosStreamWriter& Stm) const -{ - const_cast(this)->Serialize(Stm); - return true; -} - -void StraightTrajectory::OnUnlimbo(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) -{ - const auto pType = this->Type; - this->PassDetonateTimer.Start(pType->PassDetonateInitialDelay > 0 ? pType->PassDetonateInitialDelay : 0); - this->LastTargetCoord = pBullet->TargetCoords; - pBullet->Velocity = BulletVelocity::Empty; - const auto pFirer = pBullet->Owner; - - // Determine the range of the bullet - if (const auto pWeapon = pBullet->WeaponType) - { - this->AttenuationRange = pWeapon->Range; - this->CountOfBurst = pWeapon->Burst; - - if (pType->ApplyRangeModifiers && pFirer) - { - if (this->DetonationDistance >= 0) - this->DetonationDistance = Leptons(WeaponTypeExt::GetRangeWithModifiers(pWeapon, pFirer, this->DetonationDistance)); - else - this->DetonationDistance = Leptons(-WeaponTypeExt::GetRangeWithModifiers(pWeapon, pFirer, -this->DetonationDistance)); - - this->AttenuationRange = WeaponTypeExt::GetRangeWithModifiers(pWeapon, pFirer); - } - } - - // Record some information of the attacker - if (pFirer) - { - this->CurrentBurst = pFirer->CurrentBurstIndex; - this->FirepowerMult = TechnoExt::GetCurrentFirepowerMultiplier(pFirer); - - if (pType->MirrorCoord && pFirer->CurrentBurstIndex % 2 == 1) - this->OffsetCoord.Y = -(this->OffsetCoord.Y); - } - - // Wait, or launch immediately? - if (!pType->LeadTimeCalculate || !abstract_cast(pBullet->Target)) - this->PrepareForOpenFire(pBullet); - else - this->WaitOneFrame = 2; -} - -bool StraightTrajectory::OnAI(BulletClass* pBullet) -{ - if (this->WaitOneFrame && this->BulletPrepareCheck(pBullet)) - return false; - - const auto pOwner = pBullet->Owner ? pBullet->Owner->Owner : BulletExt::ExtMap.Find(pBullet)->FirerHouse; - const auto pType = this->Type; - - if (this->BulletDetonatePreCheck(pBullet)) - return true; - - this->BulletDetonateVelocityCheck(pBullet, pOwner); - - if (pType->PassDetonate) - this->PassWithDetonateAt(pBullet, pOwner); - - if (this->ProximityImpact != 0 && pType->ProximityRadius.Get() > 0) - this->PrepareForDetonateAt(pBullet, pOwner); - - if (pType->Trajectory_Speed < 256.0 && pType->ConfineAtHeight > 0 && this->PassAndConfineAtHeight(pBullet)) - return true; - - this->BulletDetonateLastCheck(pBullet, pOwner); - - return false; -} - -void StraightTrajectory::OnAIPreDetonate(BulletClass* pBullet) -{ - const auto pType = this->Type; - - // Calculate the current damage - const auto pTechno = abstract_cast(pBullet->Target); - pBullet->Health = this->GetTheTrueDamage(pBullet->Health, pBullet, pTechno, true); - - // Whether to detonate at ground level? - if (pType->PassDetonateLocal) - { - CoordStruct detonateCoords = pBullet->Location; - detonateCoords.Z = MapClass::Instance.GetCellFloorHeight(detonateCoords); - pBullet->SetLocation(detonateCoords); - } - - // Can snap to target? - const auto targetSnapDistance = pType->TargetSnapDistance.Get(); - - if (pType->PassThrough || targetSnapDistance <= 0) - return; - - const auto pTarget = abstract_cast(pBullet->Target); - const auto coords = pTarget ? pTarget->GetCoords() : pBullet->Data.Location; - - // Whether to snap to target? - if (coords.DistanceFrom(pBullet->Location) <= targetSnapDistance) - { - const auto pExt = BulletExt::ExtMap.Find(pBullet); - pExt->SnappedToTarget = true; - pBullet->SetLocation(coords); - } -} - -void StraightTrajectory::OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) -{ - pSpeed->Z += BulletTypeExt::GetAdjustedGravity(pBullet->Type); // We don't want to take the gravity into account -} - -TrajectoryCheckReturnType StraightTrajectory::OnAITargetCoordCheck(BulletClass* pBullet) -{ - return TrajectoryCheckReturnType::SkipGameCheck; // No longer needed. -} - -TrajectoryCheckReturnType StraightTrajectory::OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) -{ - return TrajectoryCheckReturnType::SkipGameCheck; // Bypass game checks entirely. -} - -void StraightTrajectory::PrepareForOpenFire(BulletClass* pBullet) -{ - const auto pType = this->Type; - double rotateAngle = 0.0; - auto theTargetCoords = pBullet->TargetCoords; - auto theSourceCoords = pBullet->SourceCoords; - - // TODO If I could calculate this before firing, perhaps it can solve the problem of one frame delay and not so correct turret orientation. - if (pType->LeadTimeCalculate) - { - if (const auto pTarget = pBullet->Target) - { - theTargetCoords = pTarget->GetCoords(); - theSourceCoords = pBullet->Location; - - // Solving trigonometric functions - if (theTargetCoords != this->LastTargetCoord) - { - const auto extraOffsetCoord = theTargetCoords - this->LastTargetCoord; - const auto targetSourceCoord = theSourceCoords - theTargetCoords; - const auto lastSourceCoord = theSourceCoords - this->LastTargetCoord; - - 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->Trajectory_Speed * pType->Trajectory_Speed; - 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); - int travelTime = 0; - - 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; - } - - theTargetCoords += extraOffsetCoord * travelTime; - } - } - } - } - - // Calculate the orientation of the coordinate system - if (!pType->LeadTimeCalculate && theTargetCoords == theSourceCoords && pBullet->Owner) // For disperse. - { - const auto theOwnerCoords = pBullet->Owner->GetCoords(); - rotateAngle = Math::atan2(theTargetCoords.Y - theOwnerCoords.Y , theTargetCoords.X - theOwnerCoords.X); - } - else - { - rotateAngle = Math::atan2(theTargetCoords.Y - theSourceCoords.Y , theTargetCoords.X - theSourceCoords.X); - } - - // Add the fixed offset value - if (this->OffsetCoord != CoordStruct::Empty) - { - theTargetCoords.X += static_cast(this->OffsetCoord.X * Math::cos(rotateAngle) + this->OffsetCoord.Y * Math::sin(rotateAngle)); - theTargetCoords.Y += static_cast(this->OffsetCoord.X * Math::sin(rotateAngle) - this->OffsetCoord.Y * Math::cos(rotateAngle)); - theTargetCoords.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 * theSourceCoords.DistanceFrom(theTargetCoords); - 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); - theTargetCoords = MapClass::GetRandomCoordsNear(theTargetCoords, offsetDistance, false); - } - - // Determine the distance that the bullet can travel - if (pType->PassThrough) - { - if (this->DetonationDistance > 0) - this->RemainingDistance += static_cast(this->DetonationDistance + pType->Trajectory_Speed); - else if (this->DetonationDistance < 0) - this->RemainingDistance += static_cast(theSourceCoords.DistanceFrom(theTargetCoords) - this->DetonationDistance + pType->Trajectory_Speed); - else - this->RemainingDistance = INT_MAX; - } - else - { - this->RemainingDistance += static_cast(theSourceCoords.DistanceFrom(theTargetCoords) + pType->Trajectory_Speed); - } - - // Determine the firing velocity vector of the bullet - pBullet->TargetCoords = theTargetCoords; - pBullet->Velocity.X = static_cast(theTargetCoords.X - theSourceCoords.X); - pBullet->Velocity.Y = static_cast(theTargetCoords.Y - theSourceCoords.Y); - - if (pType->ConfineAtHeight > 0 && pType->PassDetonateLocal) - pBullet->Velocity.Z = 0; - else - pBullet->Velocity.Z = static_cast(this->GetVelocityZ(pBullet)); - - // Rotate the selected angle - if (!this->UseDisperseBurst && std::abs(pType->RotateCoord) > 1e-10 && this->CountOfBurst > 1) - { - const auto axis = pType->AxisOfRotation.Get(); - - BulletVelocity rotationAxis - { - axis.X * Math::cos(rotateAngle) + axis.Y * Math::sin(rotateAngle), - axis.X * Math::sin(rotateAngle) - axis.Y * Math::cos(rotateAngle), - static_cast(axis.Z) - }; - - const auto rotationAxisLengthSquared = rotationAxis.MagnitudeSquared(); - - // Rotate around the axis of rotation - 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)) / 180; - } - else - { - extraRotate = Math::Pi * (pType->RotateCoord * (this->CurrentBurst / (this->CountOfBurst - 1.0) - 0.5)) / 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)); - } - } - - // Substitute the speed to calculate velocity - if (this->CalculateBulletVelocity(pBullet)) - this->RemainingDistance = 0; -} - -int StraightTrajectory::GetVelocityZ(BulletClass* pBullet) -{ - const auto pType = this->Type; - auto sourceCellZ = pBullet->SourceCoords.Z; - auto targetCellZ = pBullet->TargetCoords.Z; - auto 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()) - sourceCellZ += CellClass::BridgeHeight; - } - - if (const auto pTarget = abstract_cast(pBullet->Target)) - { - const auto pCell = pTarget->GetCell(); - targetCellZ = pCell->Level * Unsorted::LevelHeight; - - if (pCell->ContainsBridge()) - targetCellZ += CellClass::BridgeHeight; - } - - // If both are at the same height, use the DetonationDistance to calculate which position behind the target needs to be aimed - if (sourceCellZ == targetCellZ || std::abs(bulletVelocityZ) <= 32) - { - // Infinite distance, horizontal emission - if (!this->DetonationDistance) - return 0; - - const CoordStruct sourceCoords { pBullet->SourceCoords.X, pBullet->SourceCoords.Y, 0 }; - const CoordStruct targetCoords { pBullet->TargetCoords.X, pBullet->TargetCoords.Y, 0 }; - const auto distanceOfTwo = sourceCoords.DistanceFrom(targetCoords); - const auto theDistance = (this->DetonationDistance < 0) ? (distanceOfTwo - this->DetonationDistance) : this->DetonationDistance; - - // Calculate the ratio for subsequent speed calculation - if (std::abs(theDistance) > 1e-10) - bulletVelocityZ = static_cast(bulletVelocityZ * (distanceOfTwo / theDistance)); - else - return 0; - } - - return bulletVelocityZ; -} - -bool StraightTrajectory::CalculateBulletVelocity(BulletClass* pBullet) -{ - const auto velocityLength = pBullet->Velocity.Magnitude(); - - if (velocityLength > 1e-10) - pBullet->Velocity *= this->Type->Trajectory_Speed / velocityLength; - else - return true; - - return false; -} - -bool StraightTrajectory::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 StraightTrajectory::BulletDetonatePreCheck(BulletClass* pBullet) -{ - // If this value is not empty, it means that the projectile should be directly detonated at this time. This cannot be taken out here for use. - if (this->ExtraCheck) - return true; - - // Check the remaining travel distance of the bullet - this->RemainingDistance -= static_cast(this->Type->Trajectory_Speed); - - if (this->RemainingDistance < 0) - return true; - - const auto pType = this->Type; - - // Need to detonate it in advance? - if (!pType->PassThrough && pBullet->TargetCoords.DistanceFrom(pBullet->Location) < this->DetonationDistance) - return true; - - // Below ground level? (16 ->error range) - if (pType->SubjectToGround && MapClass::Instance.GetCellFloorHeight(pBullet->Location) >= (pBullet->Location.Z + 16)) - return true; - - // Out of map? - if (const auto pCell = MapClass::Instance.TryGetCellAt(pBullet->Location)) - return false; - else - return true; -} - -// If there is an obstacle on the route, the bullet should need to reduce its speed so it will not penetrate the obstacle. -void StraightTrajectory::BulletDetonateVelocityCheck(BulletClass* pBullet, HouseClass* pOwner) -{ - const auto pType = this->Type; - bool velocityCheck = false; - double locationDistance = this->RemainingDistance; - - // Check if the distance to the destination exceeds the speed limit - if (locationDistance < pType->Trajectory_Speed) - velocityCheck = true; - - const bool checkThrough = (!pType->ThroughBuilding || !pType->ThroughVehicles); - const bool checkSubject = (pType->SubjectToGround || pBullet->Type->SubjectToWalls); - - if (pType->Trajectory_Speed < 256.0) // Low speed with checkSubject was already done well. - { - // Blocked by obstacles? - if (checkThrough && this->CheckThroughAndSubjectInCell(pBullet, MapClass::Instance.GetCellAt(pBullet->Location), pOwner)) - { - locationDistance = 0.0; - velocityCheck = true; - } - } - else if (checkThrough || checkSubject) // When in high speed, it's necessary to check each cell on the path that the next frame will pass through - { - const auto& theSourceCoords = pBullet->Location; - const CoordStruct theTargetCoords - { - pBullet->Location.X + static_cast(pBullet->Velocity.X), - pBullet->Location.Y + static_cast(pBullet->Velocity.Y), - pBullet->Location.Z + static_cast(pBullet->Velocity.Z) - }; - - const auto sourceCell = CellClass::Coord2Cell(theSourceCoords); - const auto targetCell = CellClass::Coord2Cell(theTargetCoords); - const auto cellDist = sourceCell - targetCell; - const auto cellPace = CellStruct { static_cast(std::abs(cellDist.X)), static_cast(std::abs(cellDist.Y)) }; - - auto largePace = static_cast(std::max(cellPace.X, cellPace.Y)); - const auto stepCoord = !largePace ? CoordStruct::Empty : (theTargetCoords - theSourceCoords) * (1.0 / largePace); - auto curCoord = theSourceCoords; - auto pCurCell = MapClass::Instance.GetCellAt(sourceCell); - double cellDistance = locationDistance; - - for (size_t i = 0; i < largePace; ++i) - { - if ((pType->SubjectToGround && (curCoord.Z + 16) < MapClass::Instance.GetCellFloorHeight(curCoord)) // Below ground level? (16 ->error range) - || (pBullet->Type->SubjectToWalls && pCurCell->OverlayTypeIndex != -1 && OverlayTypeClass::Array.GetItem(pCurCell->OverlayTypeIndex)->Wall) // Impact on the wall? - || (checkThrough && this->CheckThroughAndSubjectInCell(pBullet, pCurCell, pOwner))) // Blocked by obstacles? - { - velocityCheck = true; - cellDistance = curCoord.DistanceFrom(theSourceCoords); - break; - } - - curCoord += stepCoord; - pCurCell = MapClass::Instance.GetCellAt(curCoord); - } - - locationDistance = cellDistance; - } - - // Check if the bullet needs to slow down the speed - if (velocityCheck) - { - this->RemainingDistance = 0; - locationDistance += 32.0; - - if (locationDistance < pType->Trajectory_Speed) - pBullet->Velocity *= (locationDistance / pType->Trajectory_Speed); - } -} - -// If the check result here is true, it only needs to be detonated in the next frame, without returning. -void StraightTrajectory::BulletDetonateLastCheck(BulletClass* pBullet, HouseClass* pOwner) -{ - const auto pType = this->Type; - - // Obstacles were detected in the current frame here - if (const auto pDetonateAt = this->ExtraCheck) - { - // Slow down and reset the target - const auto position = pDetonateAt->GetCoords(); - const auto distance = position.DistanceFrom(pBullet->Location); - const auto velocity = pBullet->Velocity.Magnitude(); - - pBullet->SetTarget(pDetonateAt); - pBullet->TargetCoords = position; - - if (std::abs(velocity) > 1e-10 && distance < velocity) - pBullet->Velocity *= distance / velocity; - - // Need to cause additional damage? - if (this->ProximityImpact != 0) - { - const auto pWH = pType->ProximityWarhead; - - if (!pWH) - return; - - auto damage = this->GetTheTrueDamage(this->ProximityDamage, pBullet, pType->ProximityMedial ? nullptr : pDetonateAt, false); - - if (pType->ProximityDirect) - pDetonateAt->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, position, pBullet->Owner, damage, pOwner, pDetonateAt); - - this->CalculateNewDamage(pBullet); - } - } -} - -bool StraightTrajectory::CheckThroughAndSubjectInCell(BulletClass* pBullet, CellClass* pCell, HouseClass* pOwner) -{ - const auto pType = this->Type; - auto pObject = pCell->GetContent(); - - while (pObject) - { - const auto pTechno = abstract_cast(pObject); - pObject = pObject->NextObject; - - // Non technos and not target friendly forces will be excluded - if (!pTechno || (pOwner && pOwner->IsAlliedWith(pTechno->Owner) && pTechno != pBullet->Target)) - continue; - - const auto technoType = pTechno->WhatAmI(); - - // Check building obstacles - if (technoType == 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 && (technoType == AbstractType::Unit || technoType == AbstractType::Aircraft)) - { - this->ExtraCheck = pTechno; - return true; - } - } - - return false; -} - -void StraightTrajectory::CalculateNewDamage(BulletClass* pBullet) -{ - const auto pType = this->Type; - const auto ratio = pType->CountAttenuation.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) - { - if (pBullet->Health) - { - if (const auto newDamage = static_cast(pBullet->Health * ratio)) - pBullet->Health = newDamage; - else - pBullet->Health = Math::sgn(pBullet->Health); - } - - if (this->ProximityDamage) - { - if (const auto newDamage = static_cast(this->ProximityDamage * ratio)) - this->ProximityDamage = newDamage; - else - this->ProximityDamage = Math::sgn(this->ProximityDamage); - } - - if (this->PassDetonateDamage) - { - if (const auto newDamage = static_cast(this->PassDetonateDamage * ratio)) - this->PassDetonateDamage = newDamage; - else - this->PassDetonateDamage = Math::sgn(this->PassDetonateDamage); - } - } - else - { - pBullet->Health = 0; - this->ProximityDamage = 0; - this->PassDetonateDamage = 0; - } - } -} - -void StraightTrajectory::PassWithDetonateAt(BulletClass* pBullet, HouseClass* pOwner) -{ - if (this->PassDetonateTimer.Completed()) - { - const auto pType = this->Type; - const auto pWH = pType->PassDetonateWarhead; - - if (!pWH) - return; - - this->PassDetonateTimer.Start(pType->PassDetonateDelay > 0 ? pType->PassDetonateDelay : 1); - auto detonateCoords = pBullet->Location; - - // Whether to detonate at ground level? - if (pType->PassDetonateLocal) - detonateCoords.Z = MapClass::Instance.GetCellFloorHeight(detonateCoords); - - const auto damage = this->GetTheTrueDamage(this->PassDetonateDamage, pBullet, nullptr, false); - WarheadTypeExt::DetonateAt(pWH, detonateCoords, pBullet->Owner, damage, pOwner); - this->CalculateNewDamage(pBullet); - } -} - -// Select suitable targets and choose the closer targets then attack each target only once. -void StraightTrajectory::PrepareForDetonateAt(BulletClass* pBullet, HouseClass* pOwner) -{ - const auto pType = this->Type; - const auto pWH = pType->ProximityWarhead; - - if (!pWH) - return; - - // Step 1: Find valid targets on the ground within range. - const auto radius = pType->ProximityRadius.Get(); - std::vector recCellClass = PhobosTrajectoryType::GetCellsInProximityRadius(pBullet, radius); - const size_t cellSize = recCellClass.size() * 2; - size_t vectSize = cellSize; - size_t thisSize = 0; - - const CoordStruct velocityCrd - { - static_cast(pBullet->Velocity.X), - static_cast(pBullet->Velocity.Y), - static_cast(pBullet->Velocity.Z) - }; - const auto velocitySq = velocityCrd.MagnitudeSquared(); - const auto pTarget = pBullet->Target; - - std::vector validTechnos; - validTechnos.reserve(vectSize); - - for (const auto& pRecCell : recCellClass) - { - auto pObject = pRecCell->GetContent(); - - while (pObject) - { - const auto pTechno = abstract_cast(pObject); - pObject = pObject->NextObject; - - if (!pTechno || !pTechno->IsAlive || !pTechno->IsOnMap || pTechno->Health <= 0 || pTechno->InLimbo || pTechno->IsSinking) - continue; - - const auto technoType = pTechno->WhatAmI(); - - if (technoType == AbstractType::Building && static_cast(pTechno)->Type->InvisibleInGame) - continue; - - // Not directly harming friendly forces - if (!pType->ProximityAllies && pOwner && pOwner->IsAlliedWith(pTechno->Owner) && pTechno != pTarget) - continue; - - // Check distance - const auto targetCrd = pTechno->GetCoords(); - const auto pathCrd = targetCrd - pBullet->SourceCoords; - - if (pathCrd * velocityCrd < 0) // In front of the techno - continue; - - const auto distanceCrd = targetCrd - pBullet->Location; - const auto nextDistanceCrd = distanceCrd - velocityCrd; - - if (nextDistanceCrd * velocityCrd > 0) // Behind the bullet - continue; - - const auto cross = distanceCrd.CrossProduct(nextDistanceCrd).MagnitudeSquared(); - const auto distance = (velocitySq > 1e-10) ? sqrt(cross / velocitySq) : distanceCrd.Magnitude(); - - if (technoType != AbstractType::Building && distance > radius) // In the cylinder - continue; - - if (thisSize >= vectSize) - { - vectSize += cellSize; - validTechnos.reserve(vectSize); - } - - validTechnos.push_back(pTechno); - thisSize += 1; - } - } - - // 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(sqrt(radius * radius + (velocitySq / 4)) / Unsorted::LeptonsPerCell)); - - for (auto pTechno = airTracker->Get(); pTechno; pTechno = airTracker->Get()) - { - if (!pTechno->IsAlive || !pTechno->IsOnMap || pTechno->Health <= 0 || pTechno->InLimbo || pTechno->IsSinking) - continue; - - // Not directly harming friendly forces - if (!pType->ProximityAllies && pOwner && pOwner->IsAlliedWith(pTechno->Owner) && pTechno != pTarget) - continue; - - // Check distance - const auto targetCrd = pTechno->GetCoords(); - const auto pathCrd = targetCrd - pBullet->SourceCoords; - - if (pathCrd * velocityCrd < 0) // In front of the techno - continue; - - const auto distanceCrd = targetCrd - pBullet->Location; - const auto nextDistanceCrd = distanceCrd - velocityCrd; - - if (nextDistanceCrd * velocityCrd > 0) // Behind the bullet - continue; - - const auto cross = distanceCrd.CrossProduct(nextDistanceCrd).MagnitudeSquared(); - const auto distance = (velocitySq > 1e-10) ? sqrt(cross / velocitySq) : distanceCrd.Magnitude(); - - if (distance > radius) // In the cylinder - continue; - - if (thisSize >= vectSize) - { - vectSize += cellSize; - validTechnos.reserve(vectSize); - } - - validTechnos.push_back(pTechno); - thisSize += 1; - } - } - - // Step 3: Record each target without repetition. - std::vector casualtyChecked; - casualtyChecked.reserve(std::max(validTechnos.size(), this->TheCasualty.size())); - - if (const auto pFirer = pBullet->Owner) - this->TheCasualty[pFirer->UniqueID] = 20; - - // Update Record - for (const auto& [ID, remainTime] : this->TheCasualty) - { - if (remainTime > 0) - this->TheCasualty[ID] = remainTime - 1; - else - casualtyChecked.push_back(ID); - } - - for (const auto& ID : casualtyChecked) - this->TheCasualty.erase(ID); - - std::vector validTargets; - validTargets.reserve(validTechnos.size()); - - // checking for duplicate - for (const auto& pTechno : validTechnos) - { - if (!this->TheCasualty.contains(pTechno->UniqueID)) - validTargets.push_back(pTechno); - - this->TheCasualty[pTechno->UniqueID] = 20; - } - - // 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 auto distanceA = pTechnoA->GetCoords().DistanceFromSquared(pBullet->SourceCoords); - const auto 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 technoType = pTechno->WhatAmI(); - - if (!pType->ThroughVehicles && (technoType == AbstractType::Unit || technoType == AbstractType::Aircraft)) - continue; - - if (technoType == AbstractType::Building && (static_cast(pTechno)->IsStrange() ? !pType->ThroughVehicles : !pType->ThroughBuilding)) - continue; - - // Cause damage - auto damage = this->GetTheTrueDamage(this->ProximityDamage, pBullet, pType->ProximityMedial ? nullptr : pTechno, false); - - if (pType->ProximityDirect) - pTechno->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, pTechno->GetCoords(), pBullet->Owner, damage, pOwner, pTechno); - - this->CalculateNewDamage(pBullet); - - if (this->ProximityImpact > 0) - --this->ProximityImpact; - } -} - -int StraightTrajectory::GetTheTrueDamage(int damage, BulletClass* pBullet, TechnoClass* pTechno, bool self) -{ - if (damage == 0) - return 0; - - const auto pType = this->Type; - - // Calculate damage distance attenuation - if (pType->EdgeAttenuation != 1.0) - { - const auto damageMultiplier = this->GetExtraDamageMultiplier(pBullet, pTechno); - const auto calculatedDamage = self ? damage * damageMultiplier : damage * this->FirepowerMult * damageMultiplier; - const auto signal = Math::sgn(calculatedDamage); - damage = static_cast(calculatedDamage); - - // Retain minimal damage - if (!damage && pType->EdgeAttenuation > 0.0) - damage = signal; - } - - return damage; -} - -double StraightTrajectory::GetExtraDamageMultiplier(BulletClass* pBullet, TechnoClass* pTechno) -{ - double distance = 0.0; - double damageMult = 1.0; - - // Here it may not be fair to the architecture - if (pTechno) - distance = pTechno->GetCoords().DistanceFrom(pBullet->SourceCoords); - else - distance = pBullet->Location.DistanceFrom(pBullet->SourceCoords); - - if (this->AttenuationRange < static_cast(distance)) - return this->Type->EdgeAttenuation; - - // Remove the first cell distance for calculation - if (distance > Unsorted::LeptonsPerCell) - damageMult += (this->Type->EdgeAttenuation - 1.0) * ((distance - Unsorted::LeptonsPerCell) / (static_cast(this->AttenuationRange - Unsorted::LeptonsPerCell))); - - return damageMult; -} - -bool StraightTrajectory::PassAndConfineAtHeight(BulletClass* pBullet) -{ - const CoordStruct futureCoords - { - pBullet->Location.X + static_cast(pBullet->Velocity.X), - pBullet->Location.Y + static_cast(pBullet->Velocity.Y), - pBullet->Location.Z + static_cast(pBullet->Velocity.Z) - }; - - auto checkDifference = MapClass::Instance.GetCellFloorHeight(futureCoords) - futureCoords.Z; - - if (MapClass::Instance.GetCellAt(futureCoords)->ContainsBridge()) - { - const auto 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) - { - const auto pType = this->Type; - pBullet->Velocity.Z += static_cast(checkDifference + pType->ConfineAtHeight); - - if (!pType->PassDetonateLocal && this->CalculateBulletVelocity(pBullet)) - return true; - } - else - { - return true; - } - - return false; -} diff --git a/src/Ext/Bullet/Trajectories/StraightTrajectory.h b/src/Ext/Bullet/Trajectories/StraightTrajectory.h deleted file mode 100644 index ca6c58bc8d..0000000000 --- a/src/Ext/Bullet/Trajectories/StraightTrajectory.h +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once - -#include "PhobosTrajectory.h" - -class StraightTrajectoryType final : public PhobosTrajectoryType -{ -public: - StraightTrajectoryType() : PhobosTrajectoryType() - , DetonationDistance { Leptons(102) } - , TargetSnapDistance { Leptons(128) } - , ApplyRangeModifiers { false } - , PassThrough { false } - , PassDetonate { false } - , PassDetonateWarhead {} - , PassDetonateDamage { 0 } - , PassDetonateDelay { 1 } - , PassDetonateInitialDelay { 0 } - , PassDetonateLocal { false } - , LeadTimeCalculate { false } - , OffsetCoord { { 0, 0, 0 } } - , RotateCoord { 0 } - , MirrorCoord { true } - , UseDisperseBurst { false } - , AxisOfRotation { { 0, 0, 1 } } - , ProximityImpact { 0 } - , ProximityWarhead {} - , ProximityDamage { 0 } - , ProximityRadius { Leptons(179) } - , ProximityDirect { false } - , ProximityMedial { false } - , ProximityAllies { false } - , ProximityFlight { false } - , ThroughVehicles { true } - , ThroughBuilding { true } - , SubjectToGround { false } - , ConfineAtHeight { 0 } - , EdgeAttenuation { 1.0 } - , CountAttenuation { 1.0 } - { } - - 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::Straight; } - - Valueable DetonationDistance; - Valueable TargetSnapDistance; - Valueable ApplyRangeModifiers; - Valueable PassThrough; - Valueable PassDetonate; - Valueable PassDetonateWarhead; - Valueable PassDetonateDamage; - Valueable PassDetonateDelay; - Valueable PassDetonateInitialDelay; - Valueable PassDetonateLocal; - Valueable LeadTimeCalculate; - Valueable OffsetCoord; - Valueable RotateCoord; - Valueable MirrorCoord; - Valueable UseDisperseBurst; - Valueable AxisOfRotation; - Valueable ProximityImpact; - Valueable ProximityWarhead; - Valueable ProximityDamage; - Valueable ProximityRadius; - Valueable ProximityDirect; - Valueable ProximityMedial; - Valueable ProximityAllies; - Valueable ProximityFlight; - Valueable ThroughVehicles; - Valueable ThroughBuilding; - Valueable SubjectToGround; - Valueable ConfineAtHeight; - Valueable EdgeAttenuation; - Valueable CountAttenuation; - -private: - template - void Serialize(T& Stm); -}; - -class StraightTrajectory final : public PhobosTrajectory -{ -public: - StraightTrajectory(noinit_t) { } - - StraightTrajectory(StraightTrajectoryType const* trajType) : Type { trajType } - , DetonationDistance { trajType->DetonationDistance } - , PassDetonateDamage { trajType->PassDetonateDamage } - , PassDetonateTimer {} - , OffsetCoord { trajType->OffsetCoord.Get() } - , UseDisperseBurst { trajType->UseDisperseBurst } - , ProximityImpact { trajType->ProximityImpact } - , ProximityDamage { trajType->ProximityDamage } - , RemainingDistance { 1 } - , ExtraCheck { nullptr } - , TheCasualty {} - , FirepowerMult { 1.0 } - , AttenuationRange { 0 } - , LastTargetCoord {} - , CurrentBurst { 0 } - , CountOfBurst { 0 } - , WaitOneFrame { 0 } - { } - - 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(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) override; - virtual bool OnAI(BulletClass* pBullet) override; - virtual void OnAIPreDetonate(BulletClass* pBullet) override; - virtual void OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) override; - virtual TrajectoryCheckReturnType OnAITargetCoordCheck(BulletClass* pBullet) override; - virtual TrajectoryCheckReturnType OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) override; - - const StraightTrajectoryType* Type; - Leptons DetonationDistance; - int PassDetonateDamage; - CDTimerClass PassDetonateTimer; - CoordStruct OffsetCoord; - bool UseDisperseBurst; - int ProximityImpact; - int ProximityDamage; - int RemainingDistance; - TechnoClass* ExtraCheck; // No taken out for use in next frame - std::map TheCasualty; // Only for recording existence - double FirepowerMult; - int AttenuationRange; - CoordStruct LastTargetCoord; - int CurrentBurst; - int CountOfBurst; - int WaitOneFrame; - -private: - template - void Serialize(T& Stm); - - void PrepareForOpenFire(BulletClass* pBullet); - int GetVelocityZ(BulletClass* pBullet); - bool CalculateBulletVelocity(BulletClass* pBullet); - bool BulletPrepareCheck(BulletClass* pBullet); - bool BulletDetonatePreCheck(BulletClass* pBullet); - void BulletDetonateVelocityCheck(BulletClass* pBullet, HouseClass* pOwner); - void BulletDetonateLastCheck(BulletClass* pBullet, HouseClass* pOwner); - bool CheckThroughAndSubjectInCell(BulletClass* pBullet, CellClass* pCell, HouseClass* pOwner); - void CalculateNewDamage(BulletClass* pBullet); - void PassWithDetonateAt(BulletClass* pBullet, HouseClass* pOwner); - void PrepareForDetonateAt(BulletClass* pBullet, HouseClass* pOwner); - int GetTheTrueDamage(int damage, BulletClass* pBullet, TechnoClass* pTechno, bool self); - double GetExtraDamageMultiplier(BulletClass* pBullet, TechnoClass* pTechno); - bool PassAndConfineAtHeight(BulletClass* pBullet); -}; diff --git a/src/Ext/BulletType/Body.cpp b/src/Ext/BulletType/Body.cpp index 447a856092..4a1daf4472 100644 --- a/src/Ext/BulletType/Body.cpp +++ b/src/Ext/BulletType/Body.cpp @@ -1,5 +1,7 @@ #include "Body.h" +#include + BulletTypeExt::ExtContainer BulletTypeExt::ExtMap; double BulletTypeExt::GetAdjustedGravity(BulletTypeClass* pType) @@ -38,6 +40,35 @@ void BulletTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->TrajectoryType.LoadFromINI(pINI, pSection); + this->LifeDuration.Read(exINI, pSection, "LifeDuration"); + this->NoTargetLifeTime.Read(exINI, pSection, "NoTargetLifeTime"); + this->CreateCapacity.Read(exINI, pSection, "CreateCapacity"); + this->PeacefulVanish.Read(exINI, pSection, "PeacefulVanish"); + this->ApplyRangeModifiers.Read(exINI, pSection, "ApplyRangeModifiers"); + + this->PassDetonate.Read(exINI, pSection, "PassDetonate"); + this->PassDetonateLocal.Read(exINI, pSection, "PassDetonateLocal"); + this->PassDetonateWarhead.Read(exINI, pSection, "PassDetonateWarhead"); + this->PassDetonateDamage.Read(exINI, pSection, "PassDetonateDamage"); + this->PassDetonateDelay.Read(exINI, pSection, "PassDetonateDelay"); + this->PassDetonateDelay = Math::max(1, this->PassDetonateDelay); + this->PassDetonateInitialDelay.Read(exINI, pSection, "PassDetonateInitialDelay"); + this->PassDetonateInitialDelay = Math::max(0, this->PassDetonateInitialDelay); + this->ProximityImpact.Read(exINI, pSection, "ProximityImpact"); + this->ProximityWarhead.Read(exINI, pSection, "ProximityWarhead"); + this->ProximityDamage.Read(exINI, pSection, "ProximityDamage"); + this->ProximityRadius.Read(exINI, pSection, "ProximityRadius"); + this->ProximityDirect.Read(exINI, pSection, "ProximityDirect"); + this->ProximityMedial.Read(exINI, pSection, "ProximityMedial"); + this->ProximityAllies.Read(exINI, pSection, "ProximityAllies"); + this->ProximityFlight.Read(exINI, pSection, "ProximityFlight"); + this->ThroughVehicles.Read(exINI, pSection, "PassThroughVehicles"); + this->ThroughBuilding.Read(exINI, pSection, "PassThroughBuilding"); + this->DamageEdgeAttenuation.Read(exINI, pSection, "DamageEdgeAttenuation"); + this->DamageEdgeAttenuation = Math::max(0.0, this->DamageEdgeAttenuation); + this->DamageCountAttenuation.Read(exINI, pSection, "DamageCountAttenuation"); + this->DamageCountAttenuation = Math::max(0.0, this->DamageCountAttenuation); + this->Shrapnel_AffectsGround.Read(exINI, pSection, "Shrapnel.AffectsGround"); this->Shrapnel_AffectsBuildings.Read(exINI, pSection, "Shrapnel.AffectsBuildings"); this->Shrapnel_UseWeaponTargeting.Read(exINI, pSection, "Shrapnel.UseWeaponTargeting"); @@ -51,6 +82,7 @@ void BulletTypeExt::ExtData::LoadFromINIFile(CCINIClass* const pINI) this->Arcing_AllowElevationInaccuracy.Read(exINI, pSection, "Arcing.AllowElevationInaccuracy"); this->ReturnWeapon.Read(exINI, pSection, "ReturnWeapon"); this->ReturnWeapon_ApplyFirepowerMult.Read(exINI, pSection, "ReturnWeapon.ApplyFirepowerMult"); + this->SubjectToSolid.Read(exINI, pSection, "SubjectToBuildings"); this->SubjectToGround.Read(exINI, pSection, "SubjectToGround"); this->Splits.Read(exINI, pSection, "Splits"); @@ -94,7 +126,7 @@ void BulletTypeExt::ExtData::TrajectoryValidation() const const char* pSection = pThis->ID; // Trajectory validation combined with other projectile behaviour. - if (this->TrajectoryType) + if (const auto pTrajType = this->TrajectoryType.get()) { if (pThis->Arcing) { @@ -119,6 +151,24 @@ void BulletTypeExt::ExtData::TrajectoryValidation() const Debug::Log("[Developer warning] [%s] has Trajectory set together with Vertical. Vertical has been set to false.\n", pSection); pThis->Vertical = false; } + + if (pThis->Arm) // 0x4E11F0 + pThis->Arm = 0; + + if (pThis->Ranged) // 0x467C1C + { + pThis->Ranged = false; + // To avoid inappropriate behavior, this will only apply to ProjectileRange + pTrajType->Ranged = true; + } + + const auto flag = pTrajType->Flag(); + + if (flag == TrajectoryFlag::Straight || flag == TrajectoryFlag::Bombard) + { + if (this->SubjectToGround) + static_cast(pTrajType)->SubjectToGround = true; + } } } @@ -134,6 +184,34 @@ void BulletTypeExt::ExtData::Serialize(T& Stm) .Process(this->Gravity) .Process(this->Vertical_AircraftFix) .Process(this->VerticalInitialFacing) + + .Process(this->TrajectoryType) + + .Process(this->LifeDuration) + .Process(this->NoTargetLifeTime) + .Process(this->CreateCapacity) + .Process(this->PeacefulVanish) + .Process(this->ApplyRangeModifiers) + + .Process(this->PassDetonate) + .Process(this->PassDetonateLocal) + .Process(this->PassDetonateWarhead) + .Process(this->PassDetonateDamage) + .Process(this->PassDetonateDelay) + .Process(this->PassDetonateInitialDelay) + .Process(this->ProximityImpact) + .Process(this->ProximityWarhead) + .Process(this->ProximityDamage) + .Process(this->ProximityRadius) + .Process(this->ProximityDirect) + .Process(this->ProximityMedial) + .Process(this->ProximityAllies) + .Process(this->ProximityFlight) + .Process(this->ThroughVehicles) + .Process(this->ThroughBuilding) + .Process(this->DamageEdgeAttenuation) + .Process(this->DamageCountAttenuation) + .Process(this->Shrapnel_AffectsGround) .Process(this->Shrapnel_AffectsBuildings) .Process(this->Shrapnel_UseWeaponTargeting) @@ -149,6 +227,7 @@ void BulletTypeExt::ExtData::Serialize(T& Stm) .Process(this->Arcing_AllowElevationInaccuracy) .Process(this->ReturnWeapon) .Process(this->ReturnWeapon_ApplyFirepowerMult) + .Process(this->SubjectToSolid) .Process(this->SubjectToGround) .Process(this->Splits) .Process(this->AirburstSpread) @@ -170,8 +249,6 @@ void BulletTypeExt::ExtData::Serialize(T& Stm) .Process(this->Parachuted_FallRate) .Process(this->Parachuted_MaxFallRate) .Process(this->BombParachute) - - .Process(this->TrajectoryType) // just keep this shit at last ; } diff --git a/src/Ext/BulletType/Body.h b/src/Ext/BulletType/Body.h index 84cbf8b235..9539f024d1 100644 --- a/src/Ext/BulletType/Body.h +++ b/src/Ext/BulletType/Body.h @@ -32,6 +32,31 @@ class BulletTypeExt TrajectoryTypePointer TrajectoryType; + Valueable LifeDuration; + Valueable NoTargetLifeTime; + Valueable CreateCapacity; + Nullable PeacefulVanish; + Valueable ApplyRangeModifiers; + + Valueable PassDetonate; + Valueable PassDetonateLocal; + Valueable PassDetonateWarhead; + Nullable PassDetonateDamage; + Valueable PassDetonateDelay; + Valueable PassDetonateInitialDelay; + Valueable ProximityImpact; + Valueable ProximityWarhead; + Nullable ProximityDamage; + Valueable ProximityRadius; + Valueable ProximityDirect; + Valueable ProximityMedial; + Valueable ProximityAllies; + Valueable ProximityFlight; + Valueable ThroughVehicles; + Valueable ThroughBuilding; + Valueable DamageEdgeAttenuation; + Valueable DamageCountAttenuation; + Valueable Shrapnel_AffectsGround; Valueable Shrapnel_AffectsBuildings; Valueable Shrapnel_UseWeaponTargeting; @@ -72,6 +97,9 @@ class BulletTypeExt Nullable Parachuted_MaxFallRate; Nullable BombParachute; + // Ares 0.1 + Valueable SubjectToSolid; + // Ares 0.7 Nullable BallisticScatter_Min; Nullable BallisticScatter_Max; @@ -86,6 +114,29 @@ class BulletTypeExt , Vertical_AircraftFix { true } , VerticalInitialFacing {} , TrajectoryType { } + , LifeDuration { 0 } + , NoTargetLifeTime { -1 } + , CreateCapacity { -1 } + , PeacefulVanish {} + , ApplyRangeModifiers { false } + , PassDetonate { false } + , PassDetonateLocal { false } + , PassDetonateWarhead {} + , PassDetonateDamage {} + , PassDetonateDelay { 1 } + , PassDetonateInitialDelay { 0 } + , ProximityImpact { 0 } + , ProximityWarhead {} + , ProximityDamage {} + , ProximityRadius { Leptons(179) } + , ProximityDirect { false } + , ProximityMedial { false } + , ProximityAllies { false } + , ProximityFlight { false } + , ThroughVehicles { true } + , ThroughBuilding { true } + , DamageEdgeAttenuation { 1.0 } + , DamageCountAttenuation { 1.0 } , Shrapnel_AffectsGround { false } , Shrapnel_AffectsBuildings { false } , Shrapnel_UseWeaponTargeting { false } @@ -101,6 +152,7 @@ class BulletTypeExt , Arcing_AllowElevationInaccuracy { true } , ReturnWeapon {} , ReturnWeapon_ApplyFirepowerMult { false } + , SubjectToSolid { false } , SubjectToGround { false } , Splits { false } , AirburstSpread { 1.5 } diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index c08ab73c44..f1bdff539b 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -902,6 +902,7 @@ void TechnoExt::ExtData::Serialize(T& Stm) .Process(this->CanCloakDuringRearm) .Process(this->WHAnimRemainingCreationInterval) .Process(this->LastWeaponType) + .Process(this->TrajectoryGroup) .Process(this->FiringObstacleCell) .Process(this->IsDetachingForCloak) .Process(this->BeControlledThreatFrame) diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index f38356513c..300be9d835 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,7 @@ class TechnoExt bool CanCloakDuringRearm; // Current rearm timer was started by DecloakToFire=no weapon. int WHAnimRemainingCreationInterval; WeaponTypeClass* LastWeaponType; + std::shared_ptr> TrajectoryGroup; CellClass* FiringObstacleCell; // Set on firing if there is an obstacle cell between target and techno, used for updating WaveClass target etc. bool IsDetachingForCloak; // Used for checking animation detaching, set to true before calling Detach_All() on techno when this anim is attached to and to false after when cloaking only. int BeControlledThreatFrame; @@ -127,6 +129,7 @@ class TechnoExt , CanCloakDuringRearm { false } , WHAnimRemainingCreationInterval { 0 } , LastWeaponType {} + , TrajectoryGroup {} , FiringObstacleCell {} , IsDetachingForCloak { false } , BeControlledThreatFrame { 0 } diff --git a/src/Ext/Techno/Hooks.Firing.cpp b/src/Ext/Techno/Hooks.Firing.cpp index ac91f6fcec..d34362beae 100644 --- a/src/Ext/Techno/Hooks.Firing.cpp +++ b/src/Ext/Techno/Hooks.Firing.cpp @@ -265,7 +265,7 @@ DEFINE_HOOK(0x5218F3, InfantryClass_WhatWeaponShouldIUse_DeployFireWeapon, 0x6) #pragma region TechnoClass_GetFireError DEFINE_HOOK(0x6FC339, TechnoClass_CanFire, 0x6) { - enum { CannotFire = 0x6FCB7E }; + enum { CannotFire = 0x6FCB7E, TemporarilyCannotFire = 0x6FCD0E }; GET(TechnoClass*, pThis, ESI); GET(WeaponTypeClass*, pWeapon, EDI); @@ -280,9 +280,12 @@ DEFINE_HOOK(0x6FC339, TechnoClass_CanFire, 0x6) if (nMoney < 0 && pThis->Owner->Available_Money() < -nMoney) return CannotFire; + const auto pBulletType = pWeapon->Projectile; + const auto pBulletTypeExt = BulletTypeExt::ExtMap.Find(pBulletType); + // AAOnly doesn't need to be checked if LandTargeting=1. - if (pThis->GetTechnoType()->LandTargeting != LandTargetingType::Land_Not_OK && pWeapon->Projectile->AA - && pTarget && !pTarget->IsInAir() && BulletTypeExt::ExtMap.Find(pWeapon->Projectile)->AAOnly) + if (pThis->GetTechnoType()->LandTargeting != LandTargetingType::Land_Not_OK && pBulletType->AA + && pTarget && !pTarget->IsInAir() && pBulletTypeExt->AAOnly) { return CannotFire; } @@ -339,6 +342,9 @@ DEFINE_HOOK(0x6FC339, TechnoClass_CanFire, 0x6) } } + if (pBulletTypeExt->CreateCapacity >= 0 && BulletExt::CheckExceededCapacity(pThis, pBulletType)) + return (pWeapon->Damage >= 0 || (pTargetTechno && pTargetTechno->GetHealthPercentage() < RulesClass::Instance->unknown_double_16F8)) ? TemporarilyCannotFire : CannotFire; + return 0; } @@ -797,13 +803,13 @@ DEFINE_HOOK(0x6F3AEB, TechnoClass_GetFLH, 0x6) GET_STACK(CoordStruct*, pCoords, STACK_OFFSET(0xD8, 0x4)); bool allowOnTurret = true; - bool useBurstMirroring = true; CoordStruct flh = CoordStruct::Empty; if (weaponIndex >= 0) { bool found = false; flh = TechnoExt::GetBurstFLH(pThis, weaponIndex, found); + if (!found) { if (auto const pInf = abstract_cast(pThis)) @@ -811,16 +817,14 @@ DEFINE_HOOK(0x6F3AEB, TechnoClass_GetFLH, 0x6) if (!found) flh = pThis->GetWeapon(weaponIndex)->FLH; - } - else - { - useBurstMirroring = false; + + if (pThis->CurrentBurstIndex % 2 != 0) + flh.Y = -flh.Y; } } else { - int index = -weaponIndex - 1; - useBurstMirroring = false; + const int index = -weaponIndex - 1; auto const pTypeExt = TechnoTypeExt::ExtMap.Find(pType); if (index < static_cast(pTypeExt->AlternateFLHs.size())) @@ -830,9 +834,6 @@ DEFINE_HOOK(0x6F3AEB, TechnoClass_GetFLH, 0x6) allowOnTurret = false; } - if (useBurstMirroring && pThis->CurrentBurstIndex % 2 != 0) - flh.Y = -flh.Y; - *pCoords = TechnoExt::GetFLHAbsoluteCoords(pThis, flh, allowOnTurret); R->EAX(pCoords); diff --git a/src/Phobos.Ext.cpp b/src/Phobos.Ext.cpp index ac02d779bf..d7df34a5c2 100644 --- a/src/Phobos.Ext.cpp +++ b/src/Phobos.Ext.cpp @@ -280,6 +280,14 @@ DEFINE_HOOK(0x67E826, LoadGame_Phobos, 0x6) return 0; } +std::unordered_map> SavegameGlobal::GlobalSharedRegistry; + +DEFINE_HOOK(0x67F7C8, LoadGame_ClearShared, 0x5) +{ + SavegameGlobal::ClearSharedRegistry(); + return 0; +} + DEFINE_HOOK(0x67D04E, GameSave_SavegameInformation, 0x7) { REF_STACK(SavegameInformation, Info, STACK_OFFSET(0x4A4, -0x3F4)); diff --git a/src/Utilities/EnumFunctions.cpp b/src/Utilities/EnumFunctions.cpp index 92e7ca5ec4..6d2ed85f3d 100644 --- a/src/Utilities/EnumFunctions.cpp +++ b/src/Utilities/EnumFunctions.cpp @@ -18,7 +18,7 @@ bool EnumFunctions::IsCellEligible(CellClass* const pCell, AffectedTarget allowe if (explicitEmptyCells) { - const auto pTechno = pCell->GetContent() ? abstract_cast(pCell->GetContent()) : nullptr; + const auto pTechno = abstract_cast(pCell->GetContent()); if (!pTechno && !(allowed & AffectedTarget::NoContent)) return false; diff --git a/src/Utilities/SavegameDef.h b/src/Utilities/SavegameDef.h index 5d9c419eff..a385ccdead 100644 --- a/src/Utilities/SavegameDef.h +++ b/src/Utilities/SavegameDef.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,13 @@ #include "Swizzle.h" #include "Debug.h" +class SavegameGlobal +{ +public: + static std::unordered_map> GlobalSharedRegistry; + static void ClearSharedRegistry() { SavegameGlobal::GlobalSharedRegistry.clear(); } +}; + namespace Savegame { template @@ -117,7 +125,6 @@ namespace Savegame return true; } - // specializations template @@ -356,6 +363,43 @@ namespace Savegame } }; + template + struct Savegame::PhobosStreamObject> + { + bool ReadFromStream(PhobosStreamReader& Stm, std::shared_ptr& Value, bool RegisterForChange) const + { + T* ptrOld = nullptr; + if (Stm.Load(ptrOld) && ptrOld) + { + std::shared_ptr ptrNew = std::make_shared(); + if (Savegame::ReadPhobosStream(Stm, *ptrNew, RegisterForChange)) + { + auto it = SavegameGlobal::GlobalSharedRegistry.find(ptrOld); + if (it != SavegameGlobal::GlobalSharedRegistry.end()) + { + Value = std::static_pointer_cast(it->second.lock()); + } + else + { + Value = ptrNew; + SavegameGlobal::GlobalSharedRegistry[ptrOld] = ptrNew; + PhobosSwizzle::RegisterChange(ptrOld, ptrNew.get()); + } + + return true; + } + } + + Value.reset(); + return true; + } + + bool WriteToStream(PhobosStreamWriter& Stm, const std::shared_ptr& Value) const + { + return PersistObject(Stm, Value.get()); + } + }; + template struct Savegame::PhobosStreamObject> { @@ -429,6 +473,20 @@ namespace Savegame } }; + template + struct Savegame::PhobosStreamObject> + { + bool ReadFromStream(PhobosStreamReader& Stm, std::pair& Value, bool RegisterForChange) const + { + return Savegame::ReadPhobosStream(Stm, Value.first, RegisterForChange) && Savegame::ReadPhobosStream(Stm, Value.second, RegisterForChange); + } + + bool WriteToStream(PhobosStreamWriter& Stm, const std::pair& Value) const + { + return Savegame::WritePhobosStream(Stm, Value.first) && Savegame::WritePhobosStream(Stm, Value.second); + } + }; + template struct Savegame::PhobosStreamObject> {