diff --git a/CREDITS.md b/CREDITS.md index dad7ba28ca..eae9cbd85f 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -391,6 +391,7 @@ This page lists all the individual contributions to the project by their author. - No turret unit turn to the target - Damage multiplier for different houses - Extended gattling rate down logic + - New Engrave trajectory - **Ollerus** - Build limit group enhancement - Customizable rocker amplitude diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 2e0629bf7c..86e02e22ab 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -38,6 +38,7 @@ + @@ -200,6 +201,7 @@ + diff --git a/docs/New-or-Enhanced-Logics.md b/docs/New-or-Enhanced-Logics.md index 709bf536fc..1cc62431dc 100644 --- a/docs/New-or-Enhanced-Logics.md +++ b/docs/New-or-Enhanced-Logics.md @@ -727,6 +727,7 @@ Currently interceptor weapons with projectiles that do not have `Inviso=true` wi - The speed of the projectile is defined by `Trajectory.Speed`, which unlike `Speed` used by `ROT` > 0 projectiles is defined on projectile not weapon. - In `Trajectory=Straight`, it refers to the whole distance speed of the projectile and it has no restrictions. - In `Trajectory=Bombard`, it refers to the initial speed of the projectile and it has no restrictions. + - In `Trajectory=Engrave`, it refers to the horizontal engrave speed of the projectile and it cannot exceed 128. Recommend set as about 40. - In `Trajectory=Parabola`, it refers to the horizontal velocity of the projectile and is only used for modes 0, 3, or 5 and it has no restrictions. In `rulesmd.ini`: @@ -917,6 +918,77 @@ Trajectory.Parabola.AxisOfRotation=0,0,1 ; integer - Forward,Lateral,Heig - Certainly, `Gravity` can also affect the trajectory. ``` +#### Engrave trajectory + +- Visually, like the thermal lance. Calling it 'trajectory' may not be appropriate. It does not read the settings on the weapon. + - `Trajectory.Engrave.SourceCoord` controls the starting point of engraving line segment. Taking the target as the coordinate center. Specifically, it will start from the firing position when set to 0,0 . The height of the point will always at ground level, unless `Trajectory.Engrave.ConfineOnGround` is set to false. + - `Trajectory.Engrave.TargetCoord` controls the end point of engraving line segment. Taking the target as the coordinate center. The height of the point will always at ground level, unless `Trajectory.Engrave.ConfineOnGround` is set to false. + - `Trajectory.Engrave.MirrorCoord` controls whether `Trajectory.Engrave.SourceCoord` and `Trajectory.Engrave.TargetCoord` need to mirror the lateral value to adapt to the current FLH. + - `Trajectory.Engrave.UseDisperseCoord` controls whether the emission position of the engrave laser need to replaced with the FLH of its superior's dispersed trajectory, which set `Trajectory.Disperse.RecordSourceCoord` to true. + - `Trajectory.Engrave.ApplyRangeModifiers` controls whether any applicable weapon range modifiers from the firer are applied to the engrave process. + - `Trajectory.Engrave.AllowFirerTurning` controls whether the projectile allow for significant changes in the orientation of the firer, otherwise it will disappear. + - `Trajectory.Engrave.Duration` controls the duration of the entire engrave process. Set to 0 will automatically use `Trajectory.Engrave.SourceCoord` and `Trajectory.Engrave.TargetCoord` to calculate the process duration. + - `Trajectory.Engrave.IsLaser` controls whether laser drawing is required. + - `Trajectory.Engrave.IsIntense` controls whether the engrave laser will be brighter and thicker. Need to set `Trajectory.Engrave.IsHouseColor` or `Trajectory.Engrave.IsSingleColor` to true. + - `Trajectory.Engrave.IsHouseColor` controls whether set the engrave laser to draw using player's team color. These lasers respect `Trajectory.Engrave.LaserThickness` and `Trajectory.Engrave.IsIntense`. + - `Trajectory.Engrave.IsSingleColor` controls whether set the engrave laser to draw using only `Trajectory.Engrave.LaserInnerColor`. These lasers respect `Trajectory.Engrave.LaserThickness` and `Trajectory.Engrave.IsIntense`. + - `Trajectory.Engrave.LaserInnerColor` controls the inner color of the engrave laser. + - `Trajectory.Engrave.LaserOuterColor` controls the outer color of the engrave laser. + - `Trajectory.Engrave.LaserOuterSpread` controls the spread color of the engrave laser. + - `Trajectory.Engrave.LaserThickness` controls the thickness of the engrave laser. Need to set `Trajectory.Engrave.IsHouseColor` or `Trajectory.Engrave.IsSingleColor` to true. + - `Trajectory.Engrave.LaserDuration` controls the duration of the engrave laser. + - `Trajectory.Engrave.LaserDelay` controls how often to draw the engrave laser. + - `Trajectory.Engrave.DamageDelay` controls how often to detonate warheads. + - `Trajectory.Engrave.ProximityImpact` controls the initial proximity fuse times. When there are enough remaining times and the projectile approaches another valid target, it will detonate a warhead defined by `Trajectory.Engrave.ProximityWarhead` on it. If the number of times is exhausted, the engraving process can still continue, but it will not detonate additional warhead as a result. This function can be cancelled by setting to 0. A negative integer means unlimited times. (You can use this to cause non repeated damage to all units encountered during the flight of the projectile.) + - `Trajectory.Engrave.ProximityWarhead` defines the warhead detonated by `Trajectory.Engrave.ProximityImpact`, and `Trajectory.Engrave.ProximityDamage` defines the damage caused by `Trajectory.Engrave.ProximityWarhead`. + - `Trajectory.Engrave.ProximityRadius` controls the range of proximity fuse. It can NOT be set as a negative integer. + - `Trajectory.Engrave.ProximityDirect` controls whether let the target receive damage instead of detonating the warhead. + - `Trajectory.Engrave.ProximityMedial` controls whether to detonate `Trajectory.Engrave.ProximityWarhead` at the bullet's location rather than the proximity target's location. + - `Trajectory.Engrave.ProximityAllies` controls whether allies will also trigger the proximity fuse. + - `Trajectory.Engrave.ProximityFlight` controls whether to count units in the air. + - `Trajectory.Engrave.ProximitySuicide` controls whether the projectile will self destruct after the number of proximity fuse times has been exhausted. If `Trajectory.Engrave.ProximityImpact` set to 0, this will not be enabled. + - `Trajectory.Engrave.ConfineOnGround` controls whether the height of the projectile will always at ground level. + + +In `rulesmd.ini`: +```ini +[SOMEPROJECTILE] ; Projectile +Trajectory=Engrave ; Trajectory type +Trajectory.Engrave.SourceCoord=0,0 ; integer - Forward,Lateral +Trajectory.Engrave.TargetCoord=0,0 ; integer - Forward,Lateral +Trajectory.Engrave.MirrorCoord=true ; boolean +Trajectory.Engrave.UseDisperseCoord=false ; boolean +Trajectory.Engrave.ApplyRangeModifiers=false ; boolean +Trajectory.Engrave.AllowFirerTurning=true ; boolean +Trajectory.Engrave.Duration=0 ; integer +Trajectory.Engrave.IsLaser=true ; boolean +Trajectory.Engrave.IsIntense=false ; boolean +Trajectory.Engrave.IsHouseColor=false ; boolean +Trajectory.Engrave.IsSingleColor=false ; boolean +Trajectory.Engrave.LaserInnerColor=0,0,0 ; integer - Red,Green,Blue +Trajectory.Engrave.LaserOuterColor=0,0,0 ; integer - Red,Green,Blue +Trajectory.Engrave.LaserOuterSpread=0,0,0 ; integer - Red,Green,Blue +Trajectory.Engrave.LaserThickness=3 ; integer +Trajectory.Engrave.LaserDuration=1 ; integer +Trajectory.Engrave.LaserDelay=1 ; integer +Trajectory.Engrave.DamageDelay=2 ; integer +Trajectory.Engrave.ProximityImpact=0 ; integer +Trajectory.Engrave.ProximityWarhead= ; WarheadType +Trajectory.Engrave.ProximityDamage=0 ; integer +Trajectory.Engrave.ProximityRadius=0.7 ; floating point value +Trajectory.Engrave.ProximityDirect=false ; boolean +Trajectory.Engrave.ProximityMedial=false ; boolean +Trajectory.Engrave.ProximityAllies=false ; boolean +Trajectory.Engrave.ProximityFlight=false ; boolean +Trajectory.Engrave.ProximitySuicide=false ; boolean +Trajectory.Engrave.ConfineOnGround=true ; boolean +``` + +```{note} +- It's best not to let it be intercepted. +- Make sure you set a low `Trajectory.Engrave.ProximityRadius` value unless necessary. +``` + ### Shrapnel enhancements ![image](_static/images/shrapnel.gif) diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 025e74b725..bc4727b49b 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -328,9 +328,11 @@ New: - Damage multiplier for different houses (by CrimRecya) - Customizable duration for electric bolts (by Starkku) - Extended gattling rate down logic (by CrimRecya) +- New Engrave trajectory (by CrimRecya) Vanilla fixes: - Prevent the units with locomotors that cause problems from entering the tank bunker (by TaranDahl) +- Fix an issue where a unit will leave an impassable invisible barrier in its original position when it is teleported by ChronoSphere onto an uncrushable unit and self destruct (by NetsuNegi) Fixes / interactions with other extensions: - Allowed `AuxBuilding` and Ares' `SW.Aux/NegBuildings` to count building upgrades (by Ollerus) diff --git a/src/Ext/Bullet/Hooks.cpp b/src/Ext/Bullet/Hooks.cpp index c876848656..4ceeb5d3b1 100644 --- a/src/Ext/Bullet/Hooks.cpp +++ b/src/Ext/Bullet/Hooks.cpp @@ -295,6 +295,7 @@ constexpr bool CheckTrajectoryCanNotAlwaysSnap(const TrajectoryFlag flag) return flag != TrajectoryFlag::Invalid; /* return flag == TrajectoryFlag::Straight || flag == TrajectoryFlag::Bombard + || flag == TrajectoryFlag::Engrave; || flag == TrajectoryFlag::Parabola;*/ } diff --git a/src/Ext/Bullet/Trajectories/EngraveTrajectory.cpp b/src/Ext/Bullet/Trajectories/EngraveTrajectory.cpp new file mode 100644 index 0000000000..28c366e06d --- /dev/null +++ b/src/Ext/Bullet/Trajectories/EngraveTrajectory.cpp @@ -0,0 +1,632 @@ +#include "EngraveTrajectory.h" + +#include +#include +#include + +#include +#include +#include +#include + +std::unique_ptr EngraveTrajectoryType::CreateInstance() const +{ + return std::make_unique(this); +} + +template +void EngraveTrajectoryType::Serialize(T& Stm) +{ + Stm + .Process(this->SourceCoord) + .Process(this->TargetCoord) + .Process(this->MirrorCoord) + .Process(this->UseDisperseCoord) + .Process(this->ApplyRangeModifiers) + .Process(this->AllowFirerTurning) + .Process(this->Duration) + .Process(this->IsLaser) + .Process(this->IsIntense) + .Process(this->IsHouseColor) + .Process(this->IsSingleColor) + .Process(this->LaserInnerColor) + .Process(this->LaserOuterColor) + .Process(this->LaserOuterSpread) + .Process(this->LaserThickness) + .Process(this->LaserDuration) + .Process(this->LaserDelay) + .Process(this->DamageDelay) + .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->ProximitySuicide) + .Process(this->ConfineOnGround) + ; +} + +bool EngraveTrajectoryType::Load(PhobosStreamReader& Stm, bool RegisterForChange) +{ + this->PhobosTrajectoryType::Load(Stm, false); + this->Serialize(Stm); + return true; +} + +bool EngraveTrajectoryType::Save(PhobosStreamWriter& Stm) const +{ + this->PhobosTrajectoryType::Save(Stm); + const_cast(this)->Serialize(Stm); + return true; +} + +void EngraveTrajectoryType::Read(CCINIClass* const pINI, const char* pSection) +{ + INI_EX exINI(pINI); + + this->Trajectory_Speed = Math::min(128.0, this->Trajectory_Speed); + this->SourceCoord.Read(exINI, pSection, "Trajectory.Engrave.SourceCoord"); + this->TargetCoord.Read(exINI, pSection, "Trajectory.Engrave.TargetCoord"); + this->MirrorCoord.Read(exINI, pSection, "Trajectory.Engrave.MirrorCoord"); + this->UseDisperseCoord.Read(exINI, pSection, "Trajectory.Engrave.UseDisperseCoord"); + this->ApplyRangeModifiers.Read(exINI, pSection, "Trajectory.Engrave.ApplyRangeModifiers"); + this->AllowFirerTurning.Read(exINI, pSection, "Trajectory.Engrave.AllowFirerTurning"); + this->Duration.Read(exINI, pSection, "Trajectory.Engrave.Duration"); + this->IsLaser.Read(exINI, pSection, "Trajectory.Engrave.IsLaser"); + this->IsIntense.Read(exINI, pSection, "Trajectory.Engrave.IsIntense"); + this->IsHouseColor.Read(exINI, pSection, "Trajectory.Engrave.IsHouseColor"); + this->IsSingleColor.Read(exINI, pSection, "Trajectory.Engrave.IsSingleColor"); + this->LaserInnerColor.Read(exINI, pSection, "Trajectory.Engrave.LaserInnerColor"); + this->LaserOuterColor.Read(exINI, pSection, "Trajectory.Engrave.LaserOuterColor"); + this->LaserOuterSpread.Read(exINI, pSection, "Trajectory.Engrave.LaserOuterSpread"); + this->LaserThickness.Read(exINI, pSection, "Trajectory.Engrave.LaserThickness"); + this->LaserThickness = Math::max(1, this->LaserThickness); + this->LaserDuration.Read(exINI, pSection, "Trajectory.Engrave.LaserDuration"); + this->LaserDuration = Math::max(1, this->LaserDuration); + this->LaserDelay.Read(exINI, pSection, "Trajectory.Engrave.LaserDelay"); + this->LaserDelay = Math::max(1, this->LaserDelay); + this->DamageDelay.Read(exINI, pSection, "Trajectory.Engrave.DamageDelay"); + this->DamageDelay = Math::max(1, this->DamageDelay); + this->ProximityImpact.Read(exINI, pSection, "Trajectory.Engrave.ProximityImpact"); + this->ProximityWarhead.Read(exINI, pSection, "Trajectory.Engrave.ProximityWarhead"); + this->ProximityDamage.Read(exINI, pSection, "Trajectory.Engrave.ProximityDamage"); + this->ProximityRadius.Read(exINI, pSection, "Trajectory.Engrave.ProximityRadius"); + this->ProximityDirect.Read(exINI, pSection, "Trajectory.Engrave.ProximityDirect"); + this->ProximityMedial.Read(exINI, pSection, "Trajectory.Engrave.ProximityMedial"); + this->ProximityAllies.Read(exINI, pSection, "Trajectory.Engrave.ProximityAllies"); + this->ProximityFlight.Read(exINI, pSection, "Trajectory.Engrave.ProximityFlight"); + this->ProximitySuicide.Read(exINI, pSection, "Trajectory.Engrave.ProximitySuicide"); + this->ConfineOnGround.Read(exINI, pSection, "Trajectory.Engrave.ConfineOnGround"); +} + +template +void EngraveTrajectory::Serialize(T& Stm) +{ + Stm + .Process(this->Type) + .Process(this->SourceCoord) + .Process(this->TargetCoord) + .Process(this->Duration) + .Process(this->LaserTimer) + .Process(this->DamageTimer) + .Process(this->TechnoInTransport) + .Process(this->NotMainWeapon) + .Process(this->FLHCoord) + .Process(this->BuildingCoord) + .Process(this->StartCoord) + .Process(this->ProximityImpact) + .Process(this->TheCasualty) + ; +} + +bool EngraveTrajectory::Load(PhobosStreamReader& Stm, bool RegisterForChange) +{ + this->Serialize(Stm); + return true; +} + +bool EngraveTrajectory::Save(PhobosStreamWriter& Stm) const +{ + const_cast(this)->Serialize(Stm); + return true; +} + +void EngraveTrajectory::OnUnlimbo(BulletClass* pBullet, CoordStruct* pCoord, BulletVelocity* pVelocity) +{ + const auto pType = this->Type; + this->LaserTimer.Start(0); + this->DamageTimer.Start(0); + this->FLHCoord = pBullet->SourceCoords; + const auto pTechno = pBullet->Owner; + + // When the launcher exists, the launcher's location will be used to calculate the direction of the coordinate axis instead of bullet's SourceCoords + if (pTechno) + { + for (auto pTrans = pTechno->Transporter; pTrans; pTrans = pTrans->Transporter) + this->TechnoInTransport = pTrans->UniqueID; + + this->GetTechnoFLHCoord(pBullet, pTechno); + this->CheckMirrorCoord(pTechno); + + const auto theSource = pTechno->GetCoords(); + const auto rotateAngle = Math::atan2(pBullet->TargetCoords.Y - theSource.Y , pBullet->TargetCoords.X - theSource.X); + this->SetEngraveDirection(pBullet, rotateAngle); + } + else + { + this->NotMainWeapon = true; + + const auto rotateAngle = Math::atan2(pBullet->TargetCoords.Y - pBullet->SourceCoords.Y , pBullet->TargetCoords.X - pBullet->SourceCoords.X); + this->SetEngraveDirection(pBullet, rotateAngle); + } + + // Substitute the speed to calculate velocity + auto coordDistance = pBullet->Velocity.Magnitude(); + pBullet->Velocity *= (coordDistance > 1e-10) ? (pType->Trajectory_Speed / coordDistance) : 0; + + // Calculate additional range + if (pType->ApplyRangeModifiers && pTechno) + { + if (const auto pWeapon = pBullet->WeaponType) + coordDistance = static_cast(WeaponTypeExt::GetRangeWithModifiers(pWeapon, pTechno, static_cast(coordDistance))); + } + + // Automatically calculate duration + if (this->Duration <= 0) + this->Duration = static_cast(coordDistance / pType->Trajectory_Speed) + 1; +} + +bool EngraveTrajectory::OnAI(BulletClass* pBullet) +{ + const auto pTechno = pBullet->Owner; + + if (!this->NotMainWeapon && this->InvalidFireCondition(pBullet, pTechno)) + return true; + + if (--this->Duration < 0 || this->PlaceOnCorrectHeight(pBullet)) + return true; + + const auto pOwner = pTechno ? pTechno->Owner : BulletExt::ExtMap.Find(pBullet)->FirerHouse; + + if (this->Type->IsLaser && this->LaserTimer.Completed()) + this->DrawEngraveLaser(pBullet, pTechno, pOwner); + + if (this->DamageTimer.Completed()) + this->DetonateLaserWarhead(pBullet, pTechno, pOwner); + + if (this->ProximityImpact != 0 && this->Type->ProximityRadius.Get() > 0) + this->PrepareForDetonateAt(pBullet, pOwner); + + return false; +} + +void EngraveTrajectory::OnAIPreDetonate(BulletClass* pBullet) +{ + //Prevent damage again. + pBullet->Health = 0; + pBullet->Limbo(); + pBullet->UnInit(); +} + +void EngraveTrajectory::OnAIVelocity(BulletClass* pBullet, BulletVelocity* pSpeed, BulletVelocity* pPosition) +{ + pSpeed->Z += BulletTypeExt::GetAdjustedGravity(pBullet->Type); +} + +TrajectoryCheckReturnType EngraveTrajectory::OnAITargetCoordCheck(BulletClass* pBullet) +{ + return TrajectoryCheckReturnType::SkipGameCheck; +} + +TrajectoryCheckReturnType EngraveTrajectory::OnAITechnoCheck(BulletClass* pBullet, TechnoClass* pTechno) +{ + return TrajectoryCheckReturnType::SkipGameCheck; +} + +void EngraveTrajectory::GetTechnoFLHCoord(BulletClass* pBullet, TechnoClass* pTechno) +{ + const auto pExt = TechnoExt::ExtMap.Find(pTechno); + + // Record the launch location, the building has an additional offset + if (!pExt || !pExt->LastWeaponType || pExt->LastWeaponType->Projectile != pBullet->Type) + { + this->NotMainWeapon = true; + return; + } + else if (pTechno->WhatAmI() == AbstractType::Building) + { + const auto pBuilding = static_cast(pTechno); + Matrix3D mtx; + mtx.MakeIdentity(); + + if (pTechno->HasTurret()) + { + TechnoTypeExt::ApplyTurretOffset(pBuilding->Type, &mtx); + mtx.RotateZ(static_cast(pTechno->TurretFacing().GetRadian<32>())); + } + + mtx.Translate(static_cast(pExt->LastWeaponFLH.X), static_cast(pExt->LastWeaponFLH.Y), static_cast(pExt->LastWeaponFLH.Z)); + const auto result = mtx.GetTranslation(); + this->BuildingCoord = pBullet->SourceCoords - pBuilding->GetCoords() - CoordStruct { static_cast(result.X), -static_cast(result.Y), static_cast(result.Z) }; + } + + this->FLHCoord = pExt->LastWeaponFLH; +} + +inline void EngraveTrajectory::CheckMirrorCoord(TechnoClass* pTechno) +{ + if (this->NotMainWeapon || !(pTechno->CurrentBurstIndex % 2)) + return; + + if (this->Type->MirrorCoord) + { + this->SourceCoord.Y = -(this->SourceCoord.Y); + this->TargetCoord.Y = -(this->TargetCoord.Y); + } +} + +void EngraveTrajectory::SetEngraveDirection(BulletClass* pBullet, double rotateAngle) +{ + auto theSource = pBullet->SourceCoords; + auto theTarget = pBullet->TargetCoords; + + // Special case: Starting from the launch position + if (this->SourceCoord.X != 0 || this->SourceCoord.Y != 0) + { + theSource = theTarget; + theSource.X += static_cast(this->SourceCoord.X * Math::cos(rotateAngle) + this->SourceCoord.Y * Math::sin(rotateAngle)); + theSource.Y += static_cast(this->SourceCoord.X * Math::sin(rotateAngle) - this->SourceCoord.Y * Math::cos(rotateAngle)); + } + + if (this->Type->ConfineOnGround) + theSource.Z = this->GetFloorCoordHeight(pBullet, theSource); + + this->StartCoord = theSource; + pBullet->SetLocation(theSource); + + theTarget.X += static_cast(this->TargetCoord.X * Math::cos(rotateAngle) + this->TargetCoord.Y * Math::sin(rotateAngle)); + theTarget.Y += static_cast(this->TargetCoord.X * Math::sin(rotateAngle) - this->TargetCoord.Y * Math::cos(rotateAngle)); + + pBullet->Velocity.X = theTarget.X - theSource.X; + pBullet->Velocity.Y = theTarget.Y - theSource.Y; + pBullet->Velocity.Z = 0; +} + +bool EngraveTrajectory::InvalidFireCondition(BulletClass* pBullet, TechnoClass* pTechno) +{ + if (!pTechno) + return true; + + for (auto pTrans = pTechno->Transporter; pTrans; pTrans = pTrans->Transporter) + pTechno = pTrans; + + if (!TechnoExt::IsActive(pTechno) || (this->TechnoInTransport && this->TechnoInTransport != pTechno->UniqueID)) + return true; + + if (this->Type->AllowFirerTurning) + return false; + + const auto SourceCrd = pTechno->GetCoords(); + const auto TargetCrd = pBullet->TargetCoords; + + const auto rotateAngle = Math::atan2(TargetCrd.Y - SourceCrd.Y , TargetCrd.X - SourceCrd.X); + const auto tgtDir = DirStruct(-rotateAngle); + + const auto& face = pTechno->HasTurret() ? pTechno->SecondaryFacing : pTechno->PrimaryFacing; + const auto curDir = face.Current(); + + return (std::abs(static_cast(static_cast(tgtDir.Raw) - static_cast(curDir.Raw))) >= 4096); +} + +int EngraveTrajectory::GetFloorCoordHeight(BulletClass* pBullet, const CoordStruct& coord) +{ + const auto pCell = MapClass::Instance->GetCellAt(coord); + const auto onFloor = MapClass::Instance->GetCellFloorHeight(coord); + const auto onBridge = pCell->ContainsBridge() ? onFloor + CellClass::BridgeHeight : onFloor; + + return (pBullet->SourceCoords.Z >= onBridge || pBullet->TargetCoords.Z >= onBridge) ? onBridge : onFloor; +} + +bool EngraveTrajectory::PlaceOnCorrectHeight(BulletClass* pBullet) +{ + if (!this->Type->ConfineOnGround) + return false; + + auto bulletCoords = pBullet->Location; + CoordStruct futureCoords + { + bulletCoords.X + static_cast(pBullet->Velocity.X), + bulletCoords.Y + static_cast(pBullet->Velocity.Y), + bulletCoords.Z + static_cast(pBullet->Velocity.Z) + }; + + // Calculate where will be located in the next frame + const auto checkDifference = this->GetFloorCoordHeight(pBullet, futureCoords) - futureCoords.Z; + + // When crossing the cliff, directly move the position of the bullet, otherwise change the vertical velocity + if (std::abs(checkDifference) >= 384) + { + if (pBullet->Type->SubjectToCliffs) + return true; + + if (checkDifference > 0) + { + bulletCoords.Z += checkDifference; + pBullet->SetLocation(bulletCoords); + } + else + { + const auto nowDifference = bulletCoords.Z - this->GetFloorCoordHeight(pBullet, bulletCoords); + + if (nowDifference >= 256) + { + bulletCoords.Z -= nowDifference; + pBullet->SetLocation(bulletCoords); + } + } + } + else + { + pBullet->Velocity.Z += checkDifference; + } + + return false; +} + +void EngraveTrajectory::DrawEngraveLaser(BulletClass* pBullet, TechnoClass* pTechno, HouseClass* pOwner) +{ + const auto pType = this->Type; + this->LaserTimer.Start(pType->LaserDelay); + auto fireCoord = pBullet->SourceCoords; + + for (auto pTrans = pTechno->Transporter; pTrans; pTrans = pTrans->Transporter) + pTechno = pTrans; + + // Considering that the CurrentBurstIndex may be different, it is not possible to call existing functions + if (!this->NotMainWeapon && pTechno && !pTechno->InLimbo) + { + if (pTechno->WhatAmI() != AbstractType::Building) + { + fireCoord = TechnoExt::GetFLHAbsoluteCoords(pTechno, this->FLHCoord, pTechno->HasTurret()); + } + else + { + const auto pBuilding = static_cast(pTechno); + Matrix3D mtx; + mtx.MakeIdentity(); + + if (pTechno->HasTurret()) + { + TechnoTypeExt::ApplyTurretOffset(pBuilding->Type, &mtx); + mtx.RotateZ(static_cast(pTechno->TurretFacing().GetRadian<32>())); + } + + mtx.Translate(static_cast(this->FLHCoord.X), static_cast(this->FLHCoord.Y), static_cast(this->FLHCoord.Z)); + const auto result = mtx.GetTranslation(); + fireCoord = pBuilding->GetCoords() + this->BuildingCoord + CoordStruct { static_cast(result.X), -static_cast(result.Y), static_cast(result.Z) }; + } + } + + // Draw laser from head to tail + if (pType->IsHouseColor || pType->IsSingleColor) + { + const auto pLaser = GameCreate(fireCoord, pBullet->Location, ((pType->IsHouseColor && pOwner) ? pOwner->LaserColor : pType->LaserInnerColor), ColorStruct { 0, 0, 0 }, ColorStruct { 0, 0, 0 }, pType->LaserDuration); + pLaser->IsHouseColor = true; + pLaser->Thickness = pType->LaserThickness; + pLaser->IsSupported = pType->IsIntense; + } + else + { + const auto pLaser = GameCreate(fireCoord, pBullet->Location, pType->LaserInnerColor, pType->LaserOuterColor, pType->LaserOuterSpread, pType->LaserDuration); + pLaser->IsHouseColor = false; + pLaser->Thickness = 3; + pLaser->IsSupported = false; + } +} + +inline void EngraveTrajectory::DetonateLaserWarhead(BulletClass* pBullet, TechnoClass* pTechno, HouseClass* pOwner) +{ + this->DamageTimer.Start(this->Type->DamageDelay); + WarheadTypeExt::DetonateAt(pBullet->WH, pBullet->Location, pTechno, pBullet->Health, pOwner); +} + +// Select suitable targets and choose the closer targets then attack each target only once. +void EngraveTrajectory::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 - this->StartCoord; + + 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 - this->StartCoord; + + 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; + + // 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],[this](TechnoClass* pTechnoA, TechnoClass* pTechnoB) + { + const auto distanceA = pTechnoA->GetCoords().DistanceFromSquared(this->StartCoord); + const auto distanceB = pTechnoB->GetCoords().DistanceFromSquared(this->StartCoord); + + // Distance priority + if (distanceA < distanceB) + return true; + + if (distanceA > distanceB) + return false; + + return pTechnoA->UniqueID < pTechnoB->UniqueID; + }); + } + + for (const auto& pTechno : validTargets) + { + // Cause damage + auto damage = pType->ProximityDamage; + + 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); + + if (this->ProximityImpact > 0 && --this->ProximityImpact == 0) + { + if (pType->ProximitySuicide) + this->Duration = 0; + + break; + } + } +} diff --git a/src/Ext/Bullet/Trajectories/EngraveTrajectory.h b/src/Ext/Bullet/Trajectories/EngraveTrajectory.h new file mode 100644 index 0000000000..c4fe89d1ce --- /dev/null +++ b/src/Ext/Bullet/Trajectories/EngraveTrajectory.h @@ -0,0 +1,136 @@ +#pragma once + +#include "PhobosTrajectory.h" + +class EngraveTrajectoryType final : public PhobosTrajectoryType +{ +public: + EngraveTrajectoryType() : PhobosTrajectoryType() + , SourceCoord { { 0, 0 } } + , TargetCoord { { 0, 0 } } + , MirrorCoord { true } + , UseDisperseCoord { false } + , ApplyRangeModifiers { false } + , AllowFirerTurning { true } + , Duration { 0 } + , IsLaser { true } + , IsIntense { false } + , IsHouseColor { false } + , IsSingleColor { false } + , LaserInnerColor { { 0, 0, 0 } } + , LaserOuterColor { { 0, 0, 0 } } + , LaserOuterSpread { { 0, 0, 0 } } + , LaserThickness { 3 } + , LaserDuration { 1 } + , LaserDelay { 1 } + , DamageDelay { 2 } + , ProximityImpact { 0 } + , ProximityWarhead {} + , ProximityDamage { 0 } + , ProximityRadius { Leptons(179) } + , ProximityDirect { false } + , ProximityMedial { false } + , ProximityAllies { false } + , ProximityFlight { false } + , ProximitySuicide { false } + , ConfineOnGround { true } + { } + + 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::Engrave; } + + Valueable SourceCoord; + Valueable TargetCoord; + Valueable MirrorCoord; + Valueable UseDisperseCoord; + Valueable ApplyRangeModifiers; + Valueable AllowFirerTurning; + Valueable Duration; + Valueable IsLaser; + Valueable IsIntense; + Valueable IsHouseColor; + Valueable IsSingleColor; + Valueable LaserInnerColor; + Valueable LaserOuterColor; + Valueable LaserOuterSpread; + Valueable LaserThickness; + Valueable LaserDuration; + Valueable LaserDelay; + Valueable DamageDelay; + Valueable ProximityImpact; + Valueable ProximityWarhead; + Valueable ProximityDamage; + Valueable ProximityRadius; + Valueable ProximityDirect; + Valueable ProximityMedial; + Valueable ProximityAllies; + Valueable ProximityFlight; + Valueable ProximitySuicide; + Valueable ConfineOnGround; + +private: + template + void Serialize(T& Stm); +}; + +class EngraveTrajectory final : public PhobosTrajectory +{ +public: + EngraveTrajectory(noinit_t) { } + + EngraveTrajectory(EngraveTrajectoryType const* trajType) : Type { trajType } + , SourceCoord { trajType->SourceCoord.Get() } + , TargetCoord { trajType->TargetCoord.Get() } + , Duration { trajType->Duration } + , LaserTimer {} + , DamageTimer {} + , TechnoInTransport { 0 } + , NotMainWeapon { false } + , FLHCoord {} + , BuildingCoord {} + , StartCoord {} + , ProximityImpact { trajType->ProximityImpact } + , TheCasualty {} + { } + + virtual bool Load(PhobosStreamReader& Stm, bool RegisterForChange) override; + virtual bool Save(PhobosStreamWriter& Stm) const override; + virtual TrajectoryFlag Flag() const override { return TrajectoryFlag::Engrave; } + 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 EngraveTrajectoryType* Type; + Point2D SourceCoord; + Point2D TargetCoord; + int Duration; + CDTimerClass LaserTimer; + CDTimerClass DamageTimer; + DWORD TechnoInTransport; + bool NotMainWeapon; + CoordStruct FLHCoord; + CoordStruct BuildingCoord; + CoordStruct StartCoord; + int ProximityImpact; + std::map TheCasualty; // Only for recording existence + + void SetEngraveDirection(BulletClass* pBullet, double rotateAngle); +private: + template + void Serialize(T& Stm); + + void GetTechnoFLHCoord(BulletClass* pBullet, TechnoClass* pTechno); + inline void CheckMirrorCoord(TechnoClass* pTechno); + bool InvalidFireCondition(BulletClass* pBullet, TechnoClass* pTechno); + int GetFloorCoordHeight(BulletClass* pBullet, const CoordStruct& coord); + bool PlaceOnCorrectHeight(BulletClass* pBullet); + void DrawEngraveLaser(BulletClass* pBullet, TechnoClass* pTechno, HouseClass* pOwner); + inline void DetonateLaserWarhead(BulletClass* pBullet, TechnoClass* pTechno, HouseClass* pOwner); + void PrepareForDetonateAt(BulletClass* pBullet, HouseClass* pOwner); +}; diff --git a/src/Ext/Bullet/Trajectories/PhobosTrajectory.cpp b/src/Ext/Bullet/Trajectories/PhobosTrajectory.cpp index 6d5e10a818..0033628767 100644 --- a/src/Ext/Bullet/Trajectories/PhobosTrajectory.cpp +++ b/src/Ext/Bullet/Trajectories/PhobosTrajectory.cpp @@ -7,6 +7,7 @@ #include "StraightTrajectory.h" #include "BombardTrajectory.h" +#include "EngraveTrajectory.h" #include "ParabolaTrajectory.h" TrajectoryTypePointer::TrajectoryTypePointer(TrajectoryFlag flag) @@ -19,6 +20,9 @@ TrajectoryTypePointer::TrajectoryTypePointer(TrajectoryFlag flag) case TrajectoryFlag::Bombard: _ptr = std::make_unique(); return; + case TrajectoryFlag::Engrave: + _ptr = std::make_unique(); + return; case TrajectoryFlag::Parabola: _ptr = std::make_unique(); return; @@ -37,6 +41,7 @@ namespace detail { {"Straight", TrajectoryFlag::Straight}, {"Bombard" ,TrajectoryFlag::Bombard}, + {"Engrave" ,TrajectoryFlag::Engrave}, {"Parabola", TrajectoryFlag::Parabola}, }; for (auto [name, flag] : FlagNames) @@ -116,6 +121,9 @@ bool TrajectoryPointer::Load(PhobosStreamReader& Stm, bool registerForChange) case TrajectoryFlag::Bombard: _ptr = std::make_unique(noinit_t {}); break; + case TrajectoryFlag::Engrave: + _ptr = std::make_unique(noinit_t {}); + break; case TrajectoryFlag::Parabola: _ptr = std::make_unique(noinit_t {}); break; diff --git a/src/Ext/Bullet/Trajectories/PhobosTrajectory.h b/src/Ext/Bullet/Trajectories/PhobosTrajectory.h index e92617bed9..65126018a5 100644 --- a/src/Ext/Bullet/Trajectories/PhobosTrajectory.h +++ b/src/Ext/Bullet/Trajectories/PhobosTrajectory.h @@ -10,6 +10,7 @@ enum class TrajectoryFlag : int Invalid = -1, Straight = 0, Bombard = 1, + Engrave = 3, Parabola = 4 }; diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 2fc559db2d..824018f73e 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -553,6 +553,8 @@ void TechnoExt::ExtData::Serialize(T& Stm) .Process(this->LastRearmWasFullDelay) .Process(this->CanCloakDuringRearm) .Process(this->WHAnimRemainingCreationInterval) + .Process(this->LastWeaponType) + .Process(this->LastWeaponFLH) .Process(this->FiringObstacleCell) .Process(this->IsDetachingForCloak) .Process(this->LastTargetID) diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index a4bcd1eb6c..9942b427e5 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -50,6 +50,8 @@ class TechnoExt bool CanCloakDuringRearm; // Current rearm timer was started by DecloakToFire=no weapon. int WHAnimRemainingCreationInterval; bool CanCurrentlyDeployIntoBuilding; // Only set on UnitClass technos with DeploysInto set in multiplayer games, recalculated once per frame so no need to serialize. + WeaponTypeClass* LastWeaponType; + CoordStruct LastWeaponFLH; 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. DWORD LastTargetID; @@ -93,6 +95,8 @@ class TechnoExt , CanCloakDuringRearm { false } , WHAnimRemainingCreationInterval { 0 } , CanCurrentlyDeployIntoBuilding { false } + , LastWeaponType {} + , LastWeaponFLH {} , FiringObstacleCell {} , IsDetachingForCloak { false } , LastTargetID { 0xFFFFFFFF } diff --git a/src/Ext/Techno/Hooks.Firing.cpp b/src/Ext/Techno/Hooks.Firing.cpp index 3f1eba405d..af71f9c3df 100644 --- a/src/Ext/Techno/Hooks.Firing.cpp +++ b/src/Ext/Techno/Hooks.Firing.cpp @@ -410,6 +410,16 @@ DEFINE_HOOK(0x6FCBE6, TechnoClass_CanFire_BridgeAAFix, 0x6) #pragma endregion #pragma region TechnoClass_Fire +DEFINE_HOOK(0x6FDD7D, TechnoClass_FireAt_UpdateWeaponType, 0x5) +{ + GET(WeaponTypeClass* const, pWeapon, EBX); + GET(TechnoClass* const, pThis, ESI); + + if (const auto pExt = TechnoExt::ExtMap.Find(pThis)) + pExt->LastWeaponType = pWeapon; + + return 0; +} DEFINE_HOOK(0x6FDDC0, TechnoClass_FireAt_DiscardAEOnFire, 0x6) { @@ -670,15 +680,27 @@ namespace BurstFLHTemp DEFINE_HOOK(0x6F3B37, TechnoClass_GetFLH_BurstFLH_1, 0x7) { GET(TechnoClass*, pThis, EBX); + GET(int, OriginalX, ECX); + GET(int, OriginalY, EBP); + GET(int, OriginalZ, EAX); GET_STACK(int, weaponIndex, STACK_OFFSET(0xD8, 0x8)); if (weaponIndex < 0) + { + auto currentPassenger = pThis->Passengers.FirstPassenger; + const auto passengerIndex = -weaponIndex - 1; + + for (int i = 0; i < passengerIndex && currentPassenger; i++) + currentPassenger = abstract_cast(currentPassenger->NextObject); + + if (const auto pPassengerExt = TechnoExt::ExtMap.Find(currentPassenger)) + pPassengerExt->LastWeaponFLH = { OriginalX, OriginalY, OriginalZ }; + return 0; + } bool FLHFound = false; - CoordStruct FLH = CoordStruct::Empty; - - FLH = TechnoExt::GetBurstFLH(pThis, weaponIndex, FLHFound); + auto FLH = TechnoExt::GetBurstFLH(pThis, weaponIndex, FLHFound); BurstFLHTemp::FLHFound = FLHFound; if (!FLHFound) @@ -689,10 +711,17 @@ DEFINE_HOOK(0x6F3B37, TechnoClass_GetFLH_BurstFLH_1, 0x7) if (FLHFound) { + if (const auto pExt = TechnoExt::ExtMap.Find(pThis)) + pExt->LastWeaponFLH = FLH; + R->ECX(FLH.X); R->EBP(FLH.Y); R->EAX(FLH.Z); } + else if (const auto pExt = TechnoExt::ExtMap.Find(pThis)) + { + pExt->LastWeaponFLH = { OriginalX, ((pThis->CurrentBurstIndex % 2 == 1) ? -OriginalY : OriginalY), OriginalZ }; + } return 0; }