diff --git a/CREDITS.md b/CREDITS.md index 9f643f27de..4f02cef927 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -385,11 +385,15 @@ This page lists all the individual contributions to the project by their author. - Enable Building Production Queue - Fix for sidebar not updating queued unit numbers when on hold - New Parabola trajectory + - Enhanced Bombard trajectory - **Ollerus** - Build limit group enhancement - Customizable rocker amplitude - Allowed `AuxBuilding` and Ares' `SW.Aux/NegBuildings` to count building upgrades - Type select for buildings (doc) + - Enhanced Bombard trajectory +- **NaotoYuuki** + - Vertical & meteor trajectory projectile prototypes - **handama** - AI script action to jump back to previous script - **TaranDahl (航味麻酱)** - Skirmish AI "sell all buildings and set all technos to hunt" behavior dehardcode diff --git a/YRpp b/YRpp index 31cf219918..e821ba32d4 160000 --- a/YRpp +++ b/YRpp @@ -1 +1 @@ -Subproject commit 31cf21991824f68f1fe5331b4612cf9b135951f6 +Subproject commit e821ba32d4fdbec43b324647cec0db2228c86ace diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 830cdd0a24..85502d80e3 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -825,13 +825,52 @@ Trajectory.Straight.CountAttenuation=1.0 ; floating point value #### Bombard trajectory -- Similar trajectory to `Straight`, but targets a coordinate above the intended target (height determined by `Trajectory.Bombard.Height`). When the projectile approaches that coordinate, it will free fall and explodes when it hits the target or ground. - -In `rulesmd.ini`: -```ini -[SOMEPROJECTILE] ; Projectile -Trajectory=Bombard ; Trajectory type -Trajectory.Bombard.Height=0.0 ; double +- Similar trajectory to `Straight`, but targets a coordinate between the attacker and intended target first. When the projectile approaches that turning point, it'll turn to the intended target and explodes when it hits the target or ground. + - `Trajectory.Bombard.Height` controls the height of the turning point. + - `Trajectory.Bombard.FallPercent` controls the distance of the turning point by its percentage of the total distance between attacker and intended target. If set to 0%, then it'll fly up vertically. If set to 100%, then it'll travel to the top of the intended target. + - For each launch the turning point percentage could add or minus a random value, which is not greater than `Trajectory.Bombard.FallPercentShift`. If set to 0%, random shift will be disabled. + - You can also makes the turning point scatter randomly in a circle with `Trajectory.Bombard.FallScatter.Max` as its radius. If set to 0, random scatter will be disabled. `Trajectory.Bombard.FallScatter.Min` can be used to determine the minimum radius of the circle. If `Trajectory.Bombard.FallScatter.Linear` set to true, the random scatter will be limited to the line that is vertical to the original direction of the projectile. + - `Trajectory.Bombard.FreeFallOnTarget` controls how it'll hit the intended target. If set to true, the projectile will be respawned above the intended target and free fall. If set to false, the projectile will travel to the intended target from the turning point. + - `Trajectory.Bombard.NoLaunch` controls whether the attacker will fire the projectile by itself. If set to true, projectile will directly fall from the turning point. + - `Trajectory.Bombard.FallSpeed` controls the initial speed of the projectile after it turns. If set to 0.0, then it'll use `Trajectory.Speed`. Can't work when `Trajectory.Bombard.FreeFallOnTarget` set to true. + - `Trajectory.Bombard.DetonationDistance` controls the maximum distance in cells from intended target (checked at start of each game frame, before the projectile moves) at which the projectile will be forced to detonate. Set to 0 to disable forced detonation (note that this can cause the projectile to overshoot the target). + - `Trajectory.Bombard.DetonationHeight` controls when the projectile is in a descending state and below the height of the launch position plus this value, it will detonate prematurely. Taking effect when it is set to non negative value. If `Trajectory.Bombard.EarlyDetonation` is set to true, it'll take effect during the ascending stage instead, which makes it detonate when its height is above the launch position plus this value. + - `Trajectory.Bombard.TargetSnapDistance` controls the maximum distance in cells from intended target the projectile can be at moment of detonation to make the projectile 'snap' on the intended target. Set to 0 to disable snapping. + - `Trajectory.Bombard.TurningPointAnims`, if set, will play an anim when the projectile reaches the turning point. If `Trajectory.Bombard.FreeFallOnTarget` is set to true, it'll be spawned above the target with the projectile together. If `Trajectory.Bombard.NoLaunch` is set to true, it'll be played at where the projectile falls, no matter if it's free fall or not. If more than one animation is listed, a random one is selected. + - `Trajectory.Bombard.LeadTimeCalculate` controls whether the projectile need to calculate the lead time of the target when firing. Note that this will not affect the facing of the turret. + - The following tags further customize the projectile's descending behaviors when `Trajectory.Bombard.FreeFallOnTarget` set to false. + - `Trajectory.Bombard.OffsetCoord` controls the offsets of the target. Projectile will aim at this position to attack. It also supports `Inaccurate=yes` and `Trajectory.Bombard.LeadTimeCalculate=true` on this basis. + - `Trajectory.Bombard.RotateCoord` controls whether to rotate the projectile's firing direction within the angle bisector of `Trajectory.Bombard.OffsetCoord` according to the weapon's `Burst`. Set to 0 to disable this function. + - `Trajectory.Bombard.MirrorCoord` controls whether `Trajectory.Bombard.OffsetCoord` need to mirror the lateral value to adapt to the current burst index. At the same time, the rotation direction calculated by `Trajectory.Bombard.RotateCoord` will also be reversed, and the rotation angle between each adjacent projectile on each side will not change as a result. + - `Trajectory.Bombard.UseDisperseBurst` controls whether the calculation of `Trajectory.Bombard.RotateCoord` is based on its superior's `Trajectory.Disperse.WeaponBurst` of the dispersed trajectory, rather than `Burst` of the weapon. If this value is not appropriate, it will result in unsatisfactory visual displays. + - `Trajectory.Bombard.AxisOfRotation` controls the rotation axis when calculating `Trajectory.Bombard.RotateCoord`. The axis will rotates with the unit orientation or the vector that from target position to the source position. + - `Trajectory.Bombard.SubjectToGround` controls whether the projectile should explode when it hits the ground. Note that this will not make AI search for suitable attack locations. + +In `rulesmd.ini`: +```ini +[SOMEPROJECTILE] ; Projectile +Trajectory=Bombard ; Trajectory type +Trajectory.Bombard.Height=0.0 ; double +Trajectory.Bombard.FallPercent=1.0 ; double +Trajectory.Bombard.FallPercentShift=0.0 ; double +Trajectory.Bombard.FallScatter.Max=0.0 ; floating point value +Trajectory.Bombard.FallScatter.Min=0.0 ; floating point value +Trajectory.Bombard.FallScatter.Linear=false ; boolean +Trajectory.Bombard.FreeFallOnTarget=true ; boolean +Trajectory.Bombard.NoLaunch=false ; boolean +Trajectory.Bombard.FallSpeed=0.0 ; double +Trajectory.Bombard.DetonationDistance=0.4 ; floating point value +Trajectory.Bombard.DetonationHeight=-1 ; integer +Trajectory.Bombard.EarlyDetonation=false ; boolean +Trajectory.Bombard.TargetSnapDistance=0.5 ; floating point value +Trajectory.Bombard.TurningPointAnims= ; list of Animation +Trajectory.Bombard.LeadTimeCalculate=false ; boolean +Trajectory.Bombard.OffsetCoord=0,0,0 ; integer - Forward,Lateral,Height +Trajectory.Bombard.RotateCoord=0 ; floating point value +Trajectory.Bombard.MirrorCoord=true ; boolean +Trajectory.Bombard.UseDisperseBurst=false ; boolean +Trajectory.Bombard.AxisOfRotation=0,0,1 ; integer - Forward,Lateral,Height +Trajectory.Bombard.SubjectToGround=false ; boolean ``` #### Parabola trajectory diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 04c3245b7d..ae461873f6 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -322,12 +322,16 @@ New: - New Parabola trajectory (by CrimRecya) - Type select for buildings (code by TaranDahl(航味麻酱), doc by Ollerus) - Raise alert when technos are taking damage (by TaranDahl) +- Enhanced Bombard trajectory (by CrimRecya & Ollerus, based on knowledge of NaotoYuuki) Vanilla fixes: - Aircraft will now behave as expected according to it's `MovementZone` and `SpeedType` when moving onto different surfaces. In particular, this fixes erratic behavior when vanilla aircraft is ordered to move onto water surface and instead the movement order changes to a shore nearby (by CrimRecya) Phobos fixes: - Type conversion on Warheads and Superweapons will no longer recursively convert units if applicable conversion pairs are listed, and only first applicable pair takes effect (by Starkku) + +Fixes / interactions with other extensions: +- Allowed `AuxBuilding` and Ares' `SW.Aux/NegBuildings` to count building upgrades (by Ollerus) ### 0.4 @@ -643,7 +647,6 @@ Fixes / interactions with other extensions: - Suppressed Ares' swizzle warning when parsing `Tags` and `TaskForces` (by Trsdy) - Fixed Academy *(Ares feature)* not working on the initial payloads *(Ares feature)* of vehicles built from a war factory (by Trsdy, supersedes Aephiex impl.) - Fixed Ares' InitialPayload not being created for vehicles spawned by trigger actions (by Trsdy) -- Allowed `AuxBuilding` and Ares' `SW.Aux/NegBuildings` to count building upgrades (by Ollerus) ### 0.3.0.1 diff --git a/src/Ext/Anim/Body.cpp b/src/Ext/Anim/Body.cpp index 20c6fece68..5e59892fe1 100644 --- a/src/Ext/Anim/Body.cpp +++ b/src/Ext/Anim/Body.cpp @@ -336,6 +336,40 @@ void AnimExt::SpawnFireAnims(AnimClass* pThis) } } +void AnimExt::CreateRandomAnim(const std::vector& AnimList, CoordStruct coords, TechnoClass* pTechno, HouseClass* pHouse, bool invoker, bool ownedObject) +{ + if (AnimList.empty()) + return; + + auto const pAnimType = AnimList[ScenarioClass::Instance->Random.RandomRanged(0, AnimList.size() - 1)]; + + if (!pAnimType) + return; + + auto const pAnim = GameCreate(pAnimType, coords); + + if (!pAnim || !pTechno) + return; + + AnimExt::SetAnimOwnerHouseKind(pAnim, pHouse ? pHouse : pTechno->Owner, nullptr, false, true); + + if (ownedObject) + pAnim->SetOwnerObject(pTechno); + + if (invoker) + { + auto const pAnimExt = AnimExt::ExtMap.Find(pAnim); + + if (!pAnimExt) + return; + + if (pHouse) + pAnimExt->SetInvoker(pTechno, pHouse); + else + pAnimExt->SetInvoker(pTechno); + } +} + // ============================= // load / save diff --git a/src/Ext/Anim/Body.h b/src/Ext/Anim/Body.h index dd6f95c4aa..d3938d9852 100644 --- a/src/Ext/Anim/Body.h +++ b/src/Ext/Anim/Body.h @@ -83,4 +83,5 @@ class AnimExt static void InvalidateTechnoPointers(TechnoClass* pTechno); static void InvalidateParticleSystemPointers(ParticleSystemClass* pParticleSystem); + static void CreateRandomAnim(const std::vector& AnimList, CoordStruct coords, TechnoClass* pTechno = nullptr, HouseClass* pHouse = nullptr, bool invoker = false, bool ownedObject = false); }; diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index b802e8deea..b18abef186 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -293,6 +293,7 @@ DEFINE_HOOK(0x46902C, BulletClass_Explode_Cluster, 0x6) constexpr bool CheckTrajectoryCanNotAlwaysSnap(const TrajectoryFlag flag) { return flag == TrajectoryFlag::Straight + || flag == TrajectoryFlag::Bombard || flag == TrajectoryFlag::Parabola; } diff --git a/src/Ext/Bullet/Trajectories/BombardTrajectory.cpp b/src/Ext/Bullet/Trajectories/BombardTrajectory.cpp index 3d98846840..640c10edc2 100644 --- a/src/Ext/Bullet/Trajectories/BombardTrajectory.cpp +++ b/src/Ext/Bullet/Trajectories/BombardTrajectory.cpp @@ -1,6 +1,10 @@ #include "BombardTrajectory.h" +#include "Memory.h" -#include +#include +#include +#include +#include std::unique_ptr BombardTrajectoryType::CreateInstance() const { @@ -10,7 +14,29 @@ std::unique_ptr BombardTrajectoryType::CreateInstance() const template void BombardTrajectoryType::Serialize(T& Stm) { - Stm.Process(this->Height); + 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) @@ -30,16 +56,52 @@ bool BombardTrajectoryType::Save(PhobosStreamWriter& Stm) const void BombardTrajectoryType::Read(CCINIClass* const pINI, const char* pSection) { INI_EX exINI(pINI); + this->Height.Read(exINI, pSection, "Trajectory.Bombard.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 (abs(this->FallSpeed.Get()) < 1e-10) + this->FallSpeed = this->Trajectory_Speed; + + 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->IsFalling) + .Process(this->Type) .Process(this->Height) - .Process(this->Speed) + .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) ; } @@ -57,47 +119,490 @@ bool BombardTrajectory::Save(PhobosStreamWriter& Stm) const void BombardTrajectory::OnUnlimbo(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) { + const BombardTrajectoryType* const 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; + this->InitialTargetCoord = pBullet->TargetCoords; + this->LastTargetCoord = pBullet->TargetCoords; + pBullet->Velocity = BulletVelocity::Empty; + + if (WeaponTypeClass* const pWeapon = pBullet->WeaponType) + this->CountOfBurst = pWeapon->Burst; - 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(this->Height - pBullet->SourceCoords.Z); - pBullet->Velocity *= this->Speed / pBullet->Velocity.Magnitude(); + if (TechnoClass* const pOwner = pBullet->Owner) + { + this->CurrentBurst = pOwner->CurrentBurstIndex; + + if (pType->MirrorCoord && pOwner->CurrentBurstIndex % 2 == 1) + this->OffsetCoord.Y = -(this->OffsetCoord.Y); + } + + if (!pType->NoLaunch || !pType->LeadTimeCalculate || !abstract_cast(pBullet->Target)) + this->PrepareForOpenFire(pBullet); + else + this->WaitOneFrame = 2; } bool BombardTrajectory::OnAI(BulletClass* pBullet) { - // Close enough - if (pBullet->TargetCoords.DistanceFrom(pBullet->Location) < 100) // This value maybe adjusted? + if (this->WaitOneFrame && this->BulletPrepareCheck(pBullet)) + return false; + + if (this->BulletDetonatePreCheck(pBullet)) + return true; + + // Extra check for trajectory falling + auto const pOwner = pBullet->Owner ? pBullet->Owner->Owner : BulletExt::ExtMap.Find(pBullet)->FirerHouse; + + if (this->IsFalling && !this->Type->FreeFallOnTarget && this->BulletDetonateRemainCheck(pBullet, pOwner)) return true; + this->BulletVelocityChange(pBullet); + return false; } +void BombardTrajectory::OnAIPreDetonate(BulletClass* pBullet) +{ + const BombardTrajectoryType* const pType = this->Type; + auto pTarget = abstract_cast(pBullet->Target); + auto pCoords = pTarget ? pTarget->GetCoords() : pBullet->Data.Location; + + if (pCoords.DistanceFrom(pBullet->Location) <= pType->TargetSnapDistance.Get()) + { + auto const pExt = BulletExt::ExtMap.Find(pBullet); + pExt->SnappedToTarget = true; + pBullet->SetLocation(pCoords); + } +} + void BombardTrajectory::OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) { - if (!this->IsFalling) + 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 BombardTrajectoryType* const pType = this->Type; + this->CalculateTargetCoords(pBullet); + + if (!pType->NoLaunch) { - pSpeed->Z += BulletTypeExt::GetAdjustedGravity(pBullet->Type); - if (pBullet->Location.Z + pBullet->Velocity.Z >= this->Height) + const CoordStruct 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); + } + else + { + this->IsFalling = true; + CoordStruct middleLocation = CoordStruct::Empty; + + if (!pType->FreeFallOnTarget) { - this->IsFalling = true; - pSpeed->X = 0.0; - pSpeed->Y = 0.0; - pSpeed->Z = 0.0; - pPosition->X = pBullet->TargetCoords.X; - pPosition->Y = pBullet->TargetCoords.Y; + 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) }; + } + + auto const pExt = BulletExt::ExtMap.Find(pBullet); + + if (pExt->LaserTrails.size()) + { + for (auto& trail : pExt->LaserTrails) + trail.LastLocation = middleLocation; + } + this->RefreshBulletLineTrail(pBullet); + + pBullet->SetLocation(middleLocation); + HouseClass* const pOwner = pBullet->Owner ? pBullet->Owner->Owner : BulletExt::ExtMap.Find(pBullet)->FirerHouse; + AnimExt::CreateRandomAnim(pType->TurningPointAnims, middleLocation, pBullet->Owner, pOwner, true); } +} + +CoordStruct BombardTrajectory::CalculateMiddleCoords(BulletClass* pBullet) +{ + const BombardTrajectoryType* const pType = this->Type; + const double 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); + 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) + }; } -TrajectoryCheckReturnType BombardTrajectory::OnAITargetCoordCheck(BulletClass* pBullet) +void BombardTrajectory::CalculateTargetCoords(BulletClass* pBullet) { - return TrajectoryCheckReturnType::ExecuteGameCheck; // Execute game checks. + const BombardTrajectoryType* const pType = this->Type; + CoordStruct theTargetCoords = pBullet->TargetCoords; + CoordStruct theSourceCoords = pBullet->SourceCoords; + + if (pType->NoLaunch) + theTargetCoords += this->CalculateBulletLeadTime(pBullet); + + pBullet->TargetCoords = theTargetCoords; + + if (!pType->LeadTimeCalculate && theTargetCoords == theSourceCoords && pBullet->Owner) //For disperse. + { + const CoordStruct 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); + } + + 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; + } + + if (pBullet->Type->Inaccurate) + { + auto const pTypeExt = BulletTypeExt::ExtMap.Find(pBullet->Type); + const double offsetMult = 0.0004 * pBullet->SourceCoords.DistanceFrom(pBullet->TargetCoords); + 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); + pBullet->TargetCoords = MapClass::GetRandomCoordsNear(pBullet->TargetCoords, offsetDistance, false); + } } -TrajectoryCheckReturnType BombardTrajectory::OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) +CoordStruct BombardTrajectory::CalculateBulletLeadTime(BulletClass* pBullet) +{ + const BombardTrajectoryType* const pType = this->Type; + CoordStruct coords = CoordStruct::Empty; + + if (pType->LeadTimeCalculate) + { + if (const AbstractClass* const pTarget = pBullet->Target) + { + const CoordStruct theTargetCoords = pTarget->GetCoords(); + const CoordStruct theSourceCoords = pBullet->Location; + + if (theTargetCoords != this->LastTargetCoord) + { + int travelTime = 0; + const CoordStruct extraOffsetCoord = theTargetCoords - this->LastTargetCoord; + const CoordStruct targetSourceCoord = theSourceCoords - theTargetCoords; + const CoordStruct 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 + { + const double theDistanceSquared = targetSourceCoord.MagnitudeSquared(); + const double targetSpeedSquared = extraOffsetCoord.MagnitudeSquared(); + const double targetSpeed = sqrt(targetSpeedSquared); + + const double crossFactor = lastSourceCoord.CrossProduct(targetSourceCoord).MagnitudeSquared(); + const double verticalDistanceSquared = crossFactor / targetSpeedSquared; + + const double horizonDistanceSquared = theDistanceSquared - verticalDistanceSquared; + const double horizonDistance = sqrt(horizonDistanceSquared); + + const double straightSpeed = pType->FreeFallOnTarget ? pType->Trajectory_Speed : pType->FallSpeed; + const double straightSpeedSquared = straightSpeed * straightSpeed; + + const double baseFactor = straightSpeedSquared - targetSpeedSquared; + const double squareFactor = baseFactor * verticalDistanceSquared + straightSpeedSquared * horizonDistanceSquared; + + if (squareFactor > 1e-10) + { + const double minusFactor = -(horizonDistance * targetSpeed); + + if (abs(baseFactor) < 1e-10) + { + travelTime = abs(horizonDistance) > 1e-10 ? (static_cast(theDistanceSquared / (2 * horizonDistance * targetSpeed)) + 1) : 0; + } + else + { + const int travelTimeM = static_cast((minusFactor - sqrt(squareFactor)) / baseFactor); + const int 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 BombardTrajectoryType* const pType = this->Type; + + if (!this->UseDisperseBurst && abs(pType->RotateCoord) > 1e-10 && this->CountOfBurst > 1) + { + const CoordStruct axis = pType->AxisOfRotation; + + 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 double rotationAxisLengthSquared = rotationAxis.MagnitudeSquared(); + + if (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 double 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 AbstractClass* const 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) { - return TrajectoryCheckReturnType::ExecuteGameCheck; // Execute game checks. + const BombardTrajectoryType* const 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, HouseClass* pOwner) +{ + const BombardTrajectoryType* const 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 BombardTrajectoryType* const pType = this->Type; + + if (!this->IsFalling) + { + if (pBullet->Location.Z + pBullet->Velocity.Z >= this->Height) + { + if (this->ToFalling) + { + this->IsFalling = true; + const AbstractClass* const pTarget = pBullet->Target; + CoordStruct middleLocation = CoordStruct::Empty; + + if (!pType->FreeFallOnTarget) + { + middleLocation = CoordStruct + { + static_cast(pBullet->Location.X + pBullet->Velocity.X), + static_cast(pBullet->Location.Y + pBullet->Velocity.Y), + static_cast(pBullet->Location.Z + pBullet->Velocity.Z) + }; + + if (pType->LeadTimeCalculate && pTarget) + pBullet->TargetCoords += pTarget->GetCoords() - this->InitialTargetCoord + this->CalculateBulletLeadTime(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 + { + 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; + } + + auto const pExt = BulletExt::ExtMap.Find(pBullet); + + if (pExt->LaserTrails.size()) + { + for (auto& trail : pExt->LaserTrails) + trail.LastLocation = middleLocation; + } + + this->RefreshBulletLineTrail(pBullet); + pBullet->SetLocation(middleLocation); + TechnoClass* const pTechno = pBullet->Owner; + AnimExt::CreateRandomAnim(pType->TurningPointAnims, middleLocation, pTechno, pTechno ? pTechno->Owner : pExt->FirerHouse, true); + } + else + { + this->ToFalling = true; + const AbstractClass* const pTarget = pBullet->Target; + + if (pType->LeadTimeCalculate && pTarget) + this->LastTargetCoord = pTarget->GetCoords(); + + pBullet->Velocity *= abs((this->Height - pBullet->Location.Z) / pBullet->Velocity.Z); + } + } + } + else if (pType->FreeFallOnTarget) + { + pBullet->Velocity.Z -= BulletTypeExt::GetAdjustedGravity(pBullet->Type); + } +} + +void BombardTrajectory::RefreshBulletLineTrail(BulletClass* pBullet) +{ + if (LineTrail* const pLineTrailer = pBullet->LineTrailer) + { + pLineTrailer->~LineTrail(); + pBullet->LineTrailer = nullptr; + } + + BulletTypeClass* const pType = pBullet->Type; + + if (pType->UseLineTrail) + { + if (LineTrail* const 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 index 8fa67a353a..a68c6dcc76 100644 --- a/src/Ext/Bullet/Trajectories/BombardTrajectory.h +++ b/src/Ext/Bullet/Trajectories/BombardTrajectory.h @@ -7,15 +7,56 @@ 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 TrajectoryFlag Flag() const override { return TrajectoryFlag::Bombard; } 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 @@ -27,25 +68,60 @@ class BombardTrajectory final : public PhobosTrajectory public: BombardTrajectory(noinit_t) { } - BombardTrajectory(BombardTrajectoryType const* trajType): IsFalling { false } + BombardTrajectory(BombardTrajectoryType const* trajType) : Type { trajType } , Height { trajType->Height } - , Speed { trajType->Trajectory_Speed } - { } + , 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 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; - bool IsFalling; + const BombardTrajectoryType* Type; double Height; - double Speed; + 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, HouseClass* pOwner); + void BulletVelocityChange(BulletClass* pBullet); + void RefreshBulletLineTrail(BulletClass* pBullet); };