Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Burst projectile retargeting #1073

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dc865b2
First commit
FS-21 Jun 1, 2023
1506095
Merge branch 'develop' into feature/burst-retarget
FS-21 Jun 1, 2023
60cbebf
Added docs
FS-21 Jun 1, 2023
82dbb40
Removed comments
FS-21 Jun 1, 2023
9d7b854
Tweak
FS-21 Jun 4, 2023
ba2e450
v2.0 and tag rename
FS-21 Sep 4, 2023
a364fe9
Small changes
FS-21 Sep 4, 2023
ef93c81
Merge branch 'develop' into feature/burst-retarget
FS-21 Sep 22, 2023
1ea2d23
Point to latest YRpp
FS-21 Sep 22, 2023
2fd3455
Merge branch 'develop' into feature/burst-retarget
FS-21 Sep 25, 2023
ab1cb83
Fixes
FS-21 Oct 3, 2023
24b2cc1
tweak
FS-21 Oct 6, 2023
6f85473
tweak
FS-21 Oct 6, 2023
68296f9
Fix crash
FS-21 Oct 6, 2023
da32f58
tweak
FS-21 Oct 7, 2023
ea7d0e3
Merge branch 'develop' into feature/burst-retarget
FS-21 Nov 9, 2023
f32981b
Moved IsUnitAvailable() in TechnoExt
FS-21 Nov 11, 2023
d9e8c2a
tweaks
FS-21 Nov 11, 2023
12ffb72
Changed name of GetRandomTarget()
FS-21 Nov 11, 2023
54f327e
Tweaks based on feedback
FS-21 Nov 16, 2023
f83e32f
Merge remote-tracking branch 'origin/develop' into feature/burst-reta…
FS-21 Mar 10, 2024
24e0606
Merge remote-tracking branch 'origin/develop' into feature/burst-reta…
FS-21 Mar 10, 2024
9336b6c
fixed weird merge
FS-21 Mar 11, 2024
d84a418
Merge branch 'develop' into feature/burst-retarget
FS-21 Mar 12, 2024
1692452
Fix crashes
FS-21 Mar 23, 2024
d4d841c
Merge branch 'develop' into feature/burst-retarget
FS-21 Apr 27, 2024
e4a625b
Fix crashes
FS-21 Apr 29, 2024
a6ea99a
Merge branch 'develop' into feature/burst-retarget
FS-21 Aug 30, 2024
bb09eb0
Fix crash
FS-21 Aug 30, 2024
5677163
Merge branch 'develop' into feature/burst-retarget
FS-21 Aug 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ This page lists all the individual contributions to the project by their author.
- Shared ammo logic
- Customizable FLH when infantry is prone or deployed
- Initial strength for cloned infantry
- `Burst.Retarget` for projectile retargeting
- **Starkku**:
- Misc. minor bugfixes & improvements
- AI script actions:
Expand Down
2 changes: 1 addition & 1 deletion YRpp
Submodule YRpp updated 1 files
+2 −2 TechnoClass.h
16 changes: 16 additions & 0 deletions docs/New-or-Enhanced-Logics.md
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,22 @@ ExtraWarheads= ; list of WarheadTypes
ExtraWarheads.DamageOverrides= ; list of integers
```

### Projectile's random target
- The firer will pick targets randomly.
- Works with missiles (no splits, airbusts, etc), cannons, lasers & spawners.
- A valid techno is required for trigger the logic.
- `OmniFire=yes` will make selectable any targets around the firer, limited by the weapon range.
- `OmniFire=no` will force the firer to pick targets in an area composed by the firer's weapon range around the original target intersected with the firer's weapon range around the firer.
- `RandomTarget.Spawners.MultipleTargets=true` gives each spawner it's own target.
- This logic should be used only in one weapon of the object.

In `rulesmd.ini`:
```ini
[SOMEWEAPON] ; WeaponType
RandomTarget=0.0 ; double or percentage
RandomTarget.Spawners.MultipleTargets=false ; boolean
```

FS-21 marked this conversation as resolved.
Show resolved Hide resolved
### Feedback weapon

![image](_static/images/feedbackweapon.gif)
Expand Down
1 change: 1 addition & 0 deletions docs/Whats-New.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ New:
- Customizable straight trajectory detonation & snap distance and pass-through option (by Starkku)
- Airstrike & spy plane fixed spawn distance & height (by Starkku)
- Allow enabling application of `Verses` and `PercentAtMax` for negative damage (by Starkku)
- `Burst.Retarget` for assigning a new target in each projectile (by FS-21)

Vanilla fixes:
- Allow AI to repair structures built from base nodes/trigger action 125/SW delivery in single player missions (by Trsdy)
Expand Down
47 changes: 47 additions & 0 deletions src/Ext/Techno/Body.Update.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <Ext/Anim/Body.h>
#include <Ext/Bullet/Body.h>
#include <Ext/House/Body.h>
#include <Ext/Script/Body.h>
FS-21 marked this conversation as resolved.
Show resolved Hide resolved
#include <Utilities/EnumFunctions.h>
#include <Utilities/AresFunctions.h>

Expand Down Expand Up @@ -719,3 +720,49 @@ void TechnoExt::UpdateSharedAmmo(TechnoClass* pThis)
}
}
}

void TechnoExt::ExtData::RefreshRandomTargets()
{
auto const pThis = this->OwnerObject();
if (!pThis)
return;

const auto pExt = TechnoExt::ExtMap.Find(pThis);
if (!pExt)
return;

if (pThis->Target && pThis->SpawnManager && pExt->CurrentRandomTarget && ScriptExt::IsUnitAvailable(static_cast<TechnoClass*>(pExt->CurrentRandomTarget), true))
{
if (pThis->SpawnManager)
{
for (auto pSpawn : pThis->SpawnManager->SpawnedNodes)
{
if (!pSpawn->Unit)
continue;

auto pSpawnExt = TechnoExt::ExtMap.Find(pSpawn->Unit);
if (!pSpawnExt)
continue;

if (!pSpawnExt->CurrentRandomTarget)
{
pSpawnExt->CurrentRandomTarget = TechnoExt::GetRandomTarget(pThis);
pSpawn->Unit->Target = pSpawnExt->CurrentRandomTarget;
}
else if (pSpawn->Status == SpawnNodeStatus::Preparing && pSpawn->Unit->IsInAir())
{
if (!pSpawn->Unit->Target && pSpawnExt->CurrentRandomTarget)
pSpawn->Unit->Target = pSpawnExt->CurrentRandomTarget;
}
}
}
}
FS-21 marked this conversation as resolved.
Show resolved Hide resolved

if (pExt->OriginalTarget && !pThis->Target && ScriptExt::IsUnitAvailable(static_cast<TechnoClass*>(pExt->OriginalTarget), true) && !pThis->IsInAir())
{
if (pExt->CurrentRandomTarget && ScriptExt::IsUnitAvailable(pExt->CurrentRandomTarget, true))
pThis->SetTarget(pExt->CurrentRandomTarget);
else
pThis->SetTarget(pExt->OriginalTarget);
}
FS-21 marked this conversation as resolved.
Show resolved Hide resolved
}
FS-21 marked this conversation as resolved.
Show resolved Hide resolved
211 changes: 211 additions & 0 deletions src/Ext/Techno/Body.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include <TacticalClass.h>

#include <Ext/House/Body.h>
#include <Ext/WeaponType/Body.h>
#include <Ext/Script/Body.h>

#include <Utilities/AresFunctions.h>
#include <Utilities/EnumFunctions.h>
Expand Down Expand Up @@ -637,6 +639,213 @@ bool TechnoExt::IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource)
return false;
}

bool TechnoExt::UpdateRandomTarget(TechnoClass* pThis)
{
if (!pThis)
return false;

int weaponIndex = pThis->SelectWeapon(pThis->Target);
auto pWeapon = pThis->GetWeapon(weaponIndex)->WeaponType;
if (!pWeapon)
return false;

const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon);
if (!pWeaponExt || pWeaponExt->RandomTarget <= 0.0)
return false;

const auto pExt = TechnoExt::ExtMap.Find(pThis);
if (!pExt)
return false;

if (pThis->GetCurrentMission() == Mission::Move)
{
pExt->CurrentRandomTarget = nullptr;
pExt->OriginalTarget = nullptr;

return false;
}

if (pExt->CurrentRandomTarget && ScriptExt::IsUnitAvailable(pExt->CurrentRandomTarget, false) && pThis->SpawnManager)
return false;

if (!pThis->Target && !ScriptExt::IsUnitAvailable(abstract_cast<TechnoClass*>(pExt->OriginalTarget), false))
{
pExt->OriginalTarget = nullptr;
return false;
}

if (pExt->OriginalTarget && !ScriptExt::IsUnitAvailable(abstract_cast<TechnoClass*>(pExt->OriginalTarget), false))
{
pExt->CurrentRandomTarget = nullptr;
pExt->OriginalTarget = nullptr;
}

if (pThis->GetCurrentMission() != Mission::Attack)
{
pExt->OriginalTarget = nullptr;
return false;
}

if (!pThis->Target)
return false;

if (pThis->DistanceFrom(pExt->OriginalTarget) > pWeapon->Range)
{
if (pThis->WhatAmI() == AbstractType::Building)
{
pThis->SetTarget(nullptr);
pExt->CurrentRandomTarget = nullptr;
pExt->OriginalTarget = nullptr;

return false;
}

pThis->SetTarget(pExt->OriginalTarget);
}

if (pThis->DistanceFrom(pThis->Target) > pWeapon->Range)
{
pThis->SetTarget(pExt->OriginalTarget);
return false;
}

auto pRandomTarget = GetRandomTarget(pThis);

if (!pRandomTarget)
return false;

pExt->OriginalTarget = !pExt->OriginalTarget ? pThis->Target : pExt->OriginalTarget;
pExt->CurrentRandomTarget = pRandomTarget;
pThis->Target = pRandomTarget;

if (pThis->SpawnManager)
{
bool isFirstSpawn = true;

for (auto pSpawn : pThis->SpawnManager->SpawnedNodes)
{
if (!pSpawn->Unit)
continue;

TechnoClass* pSpawnTarget = nullptr;

auto pSpawnExt = TechnoExt::ExtMap.Find(pSpawn->Unit);
if (!pSpawnExt)
continue;

if (isFirstSpawn)
{
pSpawnTarget = pExt->CurrentRandomTarget;

if (pWeaponExt->RandomTarget_Spawners_MultipleTargets)
isFirstSpawn = false;
}
else
{
pSpawnTarget = GetRandomTarget(pThis);

if (!pSpawnTarget)
pSpawnTarget = abstract_cast<TechnoClass*>(pExt->OriginalTarget);
}

pSpawnExt->CurrentRandomTarget = pSpawnTarget;
pSpawnExt->OriginalTarget = pExt->OriginalTarget;
}
}

return true;
FS-21 marked this conversation as resolved.
Show resolved Hide resolved
}

TechnoClass* TechnoExt::GetRandomTarget(TechnoClass* pThis)
{
TechnoClass* selection = nullptr;

if (!pThis && !pThis->Target)
return selection;

int weaponIndex = pThis->SelectWeapon(pThis->Target);
auto pWeapon = pThis->GetWeapon(weaponIndex)->WeaponType;
if (!pWeapon)
return selection;

const auto pWeaponExt = WeaponTypeExt::ExtMap.Find(pWeapon);
if (!pWeaponExt || pWeaponExt->RandomTarget <= 0.0)
return selection;

const auto pExt = TechnoExt::ExtMap.Find(pThis);
if (!pExt)
return selection;

int retargetProbability = std::min((int)round(pWeaponExt->RandomTarget * 100), 100);
int dice = ScenarioClass::Instance->Random.RandomRanged(1, 100);

if (retargetProbability < dice)
return selection;

auto pThisType = pThis->GetTechnoType();
int minimumRange = pWeapon->MinimumRange;
int range = pWeapon->Range;
int airRange = pWeapon->Range + pThisType->AirRangeBonus;
bool omniFire = pWeapon->OmniFire;
std::vector<TechnoClass*> candidates;
auto originalTarget = abstract_cast<TechnoClass*>(!pExt->OriginalTarget ? pThis->Target : pExt->OriginalTarget);
bool friendlyFire = pThis->Owner->IsAlliedWith(originalTarget);

// Looking for all valid targeting candidates
for (auto pCandidate : *TechnoClass::Array)
{
if (pCandidate == pThis
|| !ScriptExt::IsUnitAvailable(pCandidate, true)
|| pThisType->Immune
|| !EnumFunctions::IsTechnoEligible(pCandidate, pWeaponExt->CanTarget, true)
|| (!pWeapon->Projectile->AA && pCandidate->IsInAir())
|| (!pWeapon->Projectile->AG && !pCandidate->IsInAir())
|| (!friendlyFire && (pThis->Owner->IsAlliedWith(pCandidate) || ScriptExt::IsUnitMindControlledFriendly(pThis->Owner, pCandidate)))
|| pCandidate->TemporalTargetingMe
|| pCandidate->BeingWarpedOut
|| (pCandidate->GetTechnoType()->Underwater && pCandidate->GetTechnoType()->NavalTargeting == NavalTargetingType::Underwater_Never)
|| (pCandidate->GetTechnoType()->Naval && pCandidate->GetTechnoType()->NavalTargeting == NavalTargetingType::Naval_None)
|| (pCandidate->CloakState == CloakState::Cloaked && !pThisType->Naval)
|| (pCandidate->InWhichLayer() == Layer::Underground))
{
continue;
}

int distanceFromAttacker = pThis->DistanceFrom(pCandidate);
if (distanceFromAttacker < minimumRange)
continue;

if (omniFire)
{
if (pCandidate->IsInAir())
range = airRange;

if (distanceFromAttacker <= range)
candidates.push_back(pCandidate);
}
else
{
int distanceFromOriginalTargetToCandidate = pCandidate->DistanceFrom(pThis->Target);
int distanceFromOriginalTarget = pThis->DistanceFrom(pThis->Target);

if (pCandidate->IsInAir())
range = airRange;

if (distanceFromAttacker <= range && distanceFromOriginalTargetToCandidate <= distanceFromOriginalTarget)
Copy link

@coderabbitai coderabbitai bot Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GetRandomTarget function is also quite complex. Similar to the UpdateRandomTarget function, consider breaking it down into smaller functions for better readability and maintainability. For example, the part where it checks if the candidate is valid could be a separate function. This would make the code easier to understand and maintain.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also not sure what does this function do and why is it in ScriptExt: ScriptExt::IsUnitMindControlledFriendly(pThis->Owner, pCandidate)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Metadorius The function ScriptExt::IsUnitMindControlledFriendly(pThis->Owner, pCandidate) is likely used to check if a unit (pCandidate) is mind-controlled and is friendly to the owner (pThis->Owner).

The reason it's in ScriptExt might be because it's a utility function that can be used in various scripts, not just in this context. It's common to place such utility functions in a separate class or namespace (like ScriptExt) to avoid code duplication and improve code organization.

However, without more context or access to the ScriptExt class, this is just an educated guess. You might want to check the implementation of ScriptExt::IsUnitMindControlledFriendly for a more accurate understanding.

Copy link
Contributor

@Starkku Starkku Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function ScriptExt::IsUnitMindControlledFriendly() was originally placed in ScriptExt because that is the only place where it was called. If it is now being called elsewhere it should likely be relocated to TechnoExt since that would be more appropriate for function that checks TechnoClass state. Same for ScriptExt::IsUnitAvailable().

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like humans are talking. I'll be quiet for now. If you need me again, just tag @coderabbitai in a new comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually place static extension functions as if they were the non-static functions of the class that is being extended (think of C# extension methods), so if it accepts the TechnoClass, it should be in TechnoExt. I am not sure if the description you gave is accurate, I would say that it needs a clarification from @FS-21.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean ScriptExt::IsUnitMindControlledFriendly()? I don't touch what other people do, like that ScriptExt::IsUnitAvailable().
I'll move them to TechnoExt then.

candidates.push_back(pCandidate);
}
}

if (candidates.size() == 0)
return selection;

// Pick one new target from the list of targets inside the weapon range
dice = ScenarioClass::Instance->Random.RandomRanged(0, candidates.size() - 1);
selection = candidates.at(dice);

return selection;
}

// =============================
// load / save

Expand All @@ -659,6 +868,8 @@ void TechnoExt::ExtData::Serialize(T& Stm)
.Process(this->DeployFireTimer)
.Process(this->ForceFullRearmDelay)
.Process(this->WHAnimRemainingCreationInterval)
.Process(this->OriginalTarget)
.Process(this->CurrentRandomTarget)
;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serialization of OriginalTarget and CurrentRandomTarget is added, but there is no version check to ensure backward compatibility with save files that do not have these fields. This could lead to issues when loading older save files. Implement a version check to handle the absence of these fields in older save files.

}

Expand Down
10 changes: 10 additions & 0 deletions src/Ext/Techno/Body.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <Utilities/Container.h>
#include <Utilities/TemplateDef.h>
#include <Utilities/Macro.h>
#include <Utilities/EnumFunctions.h>
#include <New/Entity/ShieldClass.h>
#include <New/Entity/LaserTrailClass.h>

Expand Down Expand Up @@ -38,6 +39,8 @@ class TechnoExt
CDTimerClass DeployFireTimer;
bool ForceFullRearmDelay;
int WHAnimRemainingCreationInterval;
AbstractClass* OriginalTarget;
TechnoClass* CurrentRandomTarget;

// Used for Passengers.SyncOwner.RevertOnExit instead of TechnoClass::InitialOwner / OriginallyOwnedByHouse,
// as neither is guaranteed to point to the house the TechnoClass had prior to entering transport and cannot be safely overridden.
Expand All @@ -61,6 +64,8 @@ class TechnoExt
, DeployFireTimer {}
, ForceFullRearmDelay { false }
, WHAnimRemainingCreationInterval { 0 }
, OriginalTarget { nullptr }
, CurrentRandomTarget { nullptr }
{ }

void ApplyInterceptor();
Expand All @@ -74,12 +79,15 @@ class TechnoExt
void UpdateLaserTrails();
void InitializeLaserTrails();
void UpdateMindControlAnim();
void RefreshRandomTargets();

virtual ~ExtData() override;

virtual void InvalidatePointer(void* ptr, bool bRemoved) override
{
AnnounceInvalidPointer(OriginalPassengerOwner, ptr);
AnnounceInvalidPointer(CurrentRandomTarget, ptr);
AnnounceInvalidPointer(OriginalTarget, ptr);
}

virtual void LoadFromStream(PhobosStreamReader& Stm) override;
Expand Down Expand Up @@ -143,6 +151,8 @@ class TechnoExt
static bool ConvertToType(FootClass* pThis, TechnoTypeClass* toType);
static bool CanDeployIntoBuilding(UnitClass* pThis, bool noDeploysIntoDefaultValue = false);
static bool IsTypeImmune(TechnoClass* pThis, TechnoClass* pSource);
static bool UpdateRandomTarget(TechnoClass* pThis = nullptr);
static TechnoClass* GetRandomTarget(TechnoClass* pThis = nullptr);

// WeaponHelpers.cpp
static int PickWeaponIndex(TechnoClass* pThis, TechnoClass* pTargetTechno, AbstractClass* pTarget, int weaponIndexOne, int weaponIndexTwo, bool allowFallback = true, bool allowAAFallback = true);
Expand Down
Loading
Loading