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

Allow projectiles to penetrate through ships, dealing damage multiple times #6276

Merged
merged 35 commits into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
16d2759
Create an initial projectile penetration system
Amazinite Feb 8, 2021
3b33721
Penetrating projectiles can now impact multiple ships per frame
Amazinite Sep 30, 2021
0b2df61
Merge remote-tracking branch 'upstream/master' into projectile-penetr…
Amazinite Sep 30, 2021
e71b93a
Don't trigger multiple explosions per frame
Amazinite Sep 30, 2021
550b6be
Housekeeping
Amazinite Sep 30, 2021
0da4237
Make use of a simpler and more understandable Projectile::IsDead func…
Amazinite Oct 1, 2021
eb8f618
Pass the hits to CollisionSet as a pointer
Amazinite Oct 4, 2021
f46b943
Remove unnecessary include
Amazinite Oct 5, 2021
defee70
Allow penetrating projectiles to penetrate through asteroids
Amazinite Nov 24, 2021
b5cefb9
Count projectile penetrations down instead of up
Amazinite Nov 24, 2021
6aceee8
Fix up comments
Amazinite Nov 24, 2021
5bb3190
Merge remote-tracking branch 'upstream/master' into projectile-penetr…
Amazinite Jan 22, 2022
58a9eae
Merge remote-tracking branch 'upstream/master' into projectile-penetr…
Amazinite Mar 2, 2022
a1cfb2d
Merge remote-tracking branch 'upstream/master' into projectile-penetr…
Amazinite Mar 3, 2022
e324a72
Fix merge
Amazinite Mar 3, 2022
46ba014
Merge branch 'master' into projectile-penetration
Amazinite May 22, 2022
f63b3bd
Negative pen values = unlimited pen
Amazinite May 22, 2022
41cade3
The Quyykk method
Amazinite May 22, 2022
0f25fec
Merge branch 'master' into projectile-penetration
Amazinite Aug 13, 2023
9a28ef5
Change variable names types
Amazinite Aug 15, 2023
a8961b5
Merge branch 'master' into projectile-penetration
Amazinite Aug 19, 2023
3ab086f
Merge remote-tracking branch 'upstream/master' into projectile-penetr…
Amazinite Dec 24, 2023
ad18fc0
Revert CollisionSet changes
Amazinite Dec 24, 2023
62ba4e2
Revert Engine changes
Amazinite Dec 24, 2023
66053c9
Revert AsteroidField changes
Amazinite Dec 24, 2023
0149066
Refactor collisions
Amazinite Dec 25, 2023
a67cf1a
IsDead is true for lifetime == 0
Amazinite Dec 25, 2023
812370d
Fix missing file in the cbp
Amazinite Dec 25, 2023
4211a2d
Allow blast radius weapons to explode multiple times in a frame
Amazinite Dec 26, 2023
c643cc9
Feedback changes and updates
Amazinite Dec 29, 2023
8dbbd8d
Erroneous cbp update
Amazinite Dec 29, 2023
47fbda0
Housekeeping
Amazinite Dec 29, 2023
42664ad
And this is why we play test
Amazinite Dec 29, 2023
bdd489b
Merge branch 'master' into projectile-penetration
Amazinite Dec 30, 2023
5f6a3c3
Merge branch 'master' into projectile-penetration
Amazinite Jan 7, 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
17 changes: 10 additions & 7 deletions source/CollisionSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ PARTICULAR PURPOSE. See the GNU General Public License for more details.
#include <algorithm>
#include <cstdlib>
#include <numeric>
#include <set>
#include <string>

using namespace std;
Expand Down Expand Up @@ -127,23 +126,23 @@ void CollisionSet::Finish()

// Get the first object that collides with the given projectile. If a
// "closest hit" value is given, update that value.
Body *CollisionSet::Line(const Projectile &projectile, double *closestHit) const
Body *CollisionSet::Line(const Projectile &projectile, double *closestHit, const set<const Body *> *hits) const
{
// What objects the projectile hits depends on its government.
const Government *pGov = projectile.GetGovernment();

// Convert the start and end coordinates to integers.
Amazinite marked this conversation as resolved.
Show resolved Hide resolved
Point from = projectile.Position();
Point to = from + projectile.Velocity();
return Line(from, to, closestHit, pGov, projectile.Target());
return Line(from, to, closestHit, hits, pGov, projectile.Target());
}



// Check for collisions with a line, which may be a projectile's current
// position or its entire expected trajectory (for the auto-firing AI).
Body *CollisionSet::Line(const Point &from, const Point &to, double *closestHit,
const Government *pGov, const Body *target) const
const set<const Body *> *hits, const Government *pGov, const Body *target) const
{
int x = from.X();
int y = from.Y();
Expand Down Expand Up @@ -179,8 +178,10 @@ Body *CollisionSet::Line(const Point &from, const Point &to, double *closestHit,

// Check if this projectile can hit this object. If either the
// projectile or the object has no government, it will always hit.
// If this projectile has already hit this object, it won't hit it again.
const Government *iGov = it->body->GetGovernment();
if(it->body != target && iGov && pGov && !iGov->IsEnemy(pGov))
if((it->body != target && iGov && pGov && !iGov->IsEnemy(pGov))
|| (hits && hits->count(it->body)))
continue;

const Mask &mask = it->body->GetMask(step);
Expand Down Expand Up @@ -208,7 +209,7 @@ Body *CollisionSet::Line(const Point &from, const Point &to, double *closestHit,
warned = true;
}
Point newEnd = from + pVelocity.Unit() * USED_MAX_VELOCITY;
return Line(from, newEnd, closestHit, pGov, target);
return Line(from, newEnd, closestHit, hits, pGov, target);
}

// When stepping from one grid cell to the next, we'll go in this direction.
Expand Down Expand Up @@ -253,8 +254,10 @@ Body *CollisionSet::Line(const Point &from, const Point &to, double *closestHit,

// Check if this projectile can hit this object. If either the
// projectile or the object has no government, it will always hit.
// If this projectile has already hit this object, it won't hit it again.
const Government *iGov = it->body->GetGovernment();
if(it->body != target && iGov && pGov && !iGov->IsEnemy(pGov))
if((it->body != target && iGov && pGov && !iGov->IsEnemy(pGov))
|| (hits && hits->count(it->body)))
continue;

const Mask &mask = it->body->GetMask(step);
Expand Down
5 changes: 3 additions & 2 deletions source/CollisionSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ PARTICULAR PURPOSE. See the GNU General Public License for more details.
#ifndef COLLISION_SET_H_
#define COLLISION_SET_H_

#include <set>
#include <vector>

class Government;
Expand Down Expand Up @@ -41,11 +42,11 @@ class CollisionSet {

// Get the first object that collides with the given projectile. If a
// "closest hit" value is given, update that value.
Body *Line(const Projectile &projectile, double *closestHit = nullptr) const;
Body *Line(const Projectile &projectile, double *closestHit = nullptr, const std::set<const Body *> *hits = nullptr) const;
// Check for collisions with a line, which may be a projectile's current
// position or its entire expected trajectory (for the auto-firing AI).
Body *Line(const Point &from, const Point &to, double *closestHit = nullptr,
const Government *pGov = nullptr, const Body *target = nullptr) const;
const std::set<const Body *> *hits = nullptr, const Government *pGov = nullptr, const Body *target = nullptr) const;

// Get all objects within the given range of the given point.
const std::vector<Body *> &Circle(const Point &center, double radius) const;
Expand Down
191 changes: 104 additions & 87 deletions source/Engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1931,112 +1931,129 @@ void Engine::HandleMouseClicks()
// this is multi-threaded in the future, that will need to change.
void Engine::DoCollisions(Projectile &projectile)
{
// The asteroids can collide with projectiles, the same as any other
// object. If the asteroid turns out to be closer than the ship, it
// shields the ship (unless the projectile has a blast radius).
Point hitVelocity;
double closestHit = 1.;
shared_ptr<Ship> hit;
const Government *gov = projectile.GetGovernment();

// If this "projectile" is a ship explosion, it always explodes.
if(!gov)
closestHit = 0.;
else if(projectile.GetWeapon().IsPhasing() && projectile.Target())
{
// "Phasing" projectiles that have a target will never hit any other ship.
shared_ptr<Ship> target = projectile.TargetPtr();
if(target)
// Keep track of which ships this projectile has directly impacted this frame.
set<const Body *> hits;
bool hasHit = true;
while(!projectile.IsDead() && hasHit)
{
// The asteroids can collide with projectiles, the same as any other
// object. If the asteroid turns out to be closer than the ship, it
// shields the ship (unless the projectile has a blast radius).
double closestHit = 1.;
shared_ptr<Ship> hit;
Point hitVelocity;
hasHit = false;

// If this "projectile" is a ship explosion, it always explodes.
if(!gov)
closestHit = 0.;
else if(projectile.GetWeapon().IsPhasing() && projectile.Target())
{
Point offset = projectile.Position() - target->Position();
double range = target->GetMask(step).Collide(offset, projectile.Velocity(), target->Facing());
if(range < 1.)
// "Phasing" projectiles that have a target will never hit any other ship.
shared_ptr<Ship> target = projectile.TargetPtr();
if(target)
{
closestHit = range;
hit = target;
}
}
}
else
{
// For weapons with a trigger radius, check if any detectable object will set it off.
double triggerRadius = projectile.GetWeapon().TriggerRadius();
if(triggerRadius)
for(const Body *body : shipCollisions.Circle(projectile.Position(), triggerRadius))
if(body == projectile.Target() || (gov->IsEnemy(body->GetGovernment())
&& reinterpret_cast<const Ship *>(body)->Cloaking() < 1.))
Point offset = projectile.Position() - target->Position();
double range = target->GetMask(step).Collide(offset, projectile.Velocity(), target->Facing());
if(range < 1.)
{
closestHit = 0.;
break;
closestHit = range;
hit = target;
}

// If nothing triggered the projectile, check for collisions with ships.
if(closestHit > 0.)
{
Ship *ship = reinterpret_cast<Ship *>(shipCollisions.Line(projectile, &closestHit));
if(ship)
{
hit = ship->shared_from_this();
hitVelocity = ship->Velocity();
}
}
// "Phasing" projectiles can pass through asteroids. For all other
// projectiles, check if they've hit an asteroid that is closer than any
// ship that they have hit.
if(!projectile.GetWeapon().IsPhasing())
else
{
Body *asteroid = asteroids.Collide(projectile, &closestHit);
if(asteroid)
// For weapons with a trigger radius, check if any detectable object will set it off.
double triggerRadius = projectile.GetWeapon().TriggerRadius();
if(triggerRadius)
for(const Body *body : shipCollisions.Circle(projectile.Position(), triggerRadius))
if(body == projectile.Target() || (gov->IsEnemy(body->GetGovernment())
&& reinterpret_cast<const Ship *>(body)->Cloaking() < 1.))
{
closestHit = 0.;
break;
}

// If nothing triggered the projectile, check for collisions with ships.
if(closestHit > 0.)
{
hitVelocity = asteroid->Velocity();
hit.reset();
Ship *ship = reinterpret_cast<Ship *>(shipCollisions.Line(projectile, &closestHit, &hits));
if(ship)
{
hit = ship->shared_from_this();
hitVelocity = ship->Velocity();
// Only record direct hits. Phasing projectiles or projectiles
// that were set off by their trigger radius don't need to
// check for multiple collisions in a single frame.
hasHit = true;
}
}
// "Phasing" projectiles can pass through asteroids. For all other
// projectiles, check if they've hit an asteroid that is closer than any
// ship that they have hit.
if(!projectile.GetWeapon().IsPhasing())
{
Body *asteroid = asteroids.Collide(projectile, &closestHit);
if(asteroid)
{
hitVelocity = asteroid->Velocity();
hit.reset();
// Projectiles always die when impacting an asteroid.
projectile.Kill();
}
}
}
}

// Check if the projectile hit something.
if(closestHit < 1.)
{
// Create the explosion the given distance along the projectile's
// motion path for this step.
projectile.Explode(visuals, closestHit, hitVelocity);

// If this projectile has a blast radius, find all ships within its
// radius. Otherwise, only one is damaged.
double blastRadius = projectile.GetWeapon().BlastRadius();
bool isSafe = projectile.GetWeapon().IsSafe();
if(blastRadius)
// Check if the projectile hit something.
if(closestHit < 1.)
{
// Even friendly ships can be hit by the blast, unless it is a
// "safe" weapon.
Point hitPos = projectile.Position() + closestHit * projectile.Velocity();
for(Body *body : shipCollisions.Circle(hitPos, blastRadius))
// Create the explosion the given distance along the projectile's
// motion path for this step.
projectile.Explode(visuals, closestHit, hitVelocity);

// If this projectile has a blast radius, find all ships within its
// radius. Otherwise, only one is damaged.
double blastRadius = projectile.GetWeapon().BlastRadius();
bool isSafe = projectile.GetWeapon().IsSafe();
if(blastRadius)
{
Ship *ship = reinterpret_cast<Ship *>(body);
if(isSafe && projectile.Target() != ship && !gov->IsEnemy(ship->GetGovernment()))
continue;

int eventType = ship->TakeDamage(visuals, projectile.GetWeapon(), 1.,
projectile.DistanceTraveled(), projectile.Position(), projectile.GetGovernment(), ship != hit.get());
// Even friendly ships can be hit by the blast, unless it is a
// "safe" weapon.
Point hitPos = projectile.Position() + closestHit * projectile.Velocity();
for(Body *body : shipCollisions.Circle(hitPos, blastRadius))
{
Ship *ship = reinterpret_cast<Ship *>(body);
if(isSafe && projectile.Target() != ship && !gov->IsEnemy(ship->GetGovernment()))
continue;

int eventType = ship->TakeDamage(visuals, projectile.GetWeapon(), 1.,
projectile.DistanceTraveled(), projectile.Position(), projectile.GetGovernment(), ship != hit.get());
if(eventType)
eventQueue.emplace_back(gov, ship->shared_from_this(), eventType);
}
}
else if(hit)
{
int eventType = hit->TakeDamage(visuals, projectile.GetWeapon(), 1.,
projectile.DistanceTraveled(), projectile.Position(), projectile.GetGovernment());
if(eventType)
eventQueue.emplace_back(gov, ship->shared_from_this(), eventType);
eventQueue.emplace_back(gov, hit, eventType);
}

if(hit)
{
DoGrudge(hit, gov);
hits.insert(hit.get());
}
}
else if(hit)
{
int eventType = hit->TakeDamage(visuals, projectile.GetWeapon(), 1.,
projectile.DistanceTraveled(), projectile.Position(), projectile.GetGovernment());
if(eventType)
eventQueue.emplace_back(gov, hit, eventType);
}

if(hit)
DoGrudge(hit, gov);
}
else if(projectile.MissileStrength())

// If the projectile is still alive, give the anti-missile systems
// a chance to shoot it down.
if(!projectile.IsDead() && projectile.MissileStrength())
{
// If the projectile did not hit anything, give the anti-missile systems
// a chance to shoot it down.
for(Ship *ship : hasAntiMissile)
if(ship == projectile.Target() || gov->IsEnemy(ship->GetGovernment()))
if(ship->FireAntiMissile(projectile, visuals))
Expand Down
18 changes: 15 additions & 3 deletions source/Projectile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,17 +258,20 @@ void Projectile::Move(vector<Visual> &visuals, vector<Projectile> &projectiles)


// This projectile hit something. Create the explosion, if any. This also
// marks the projectile as needing deletion.
// marks the projectile as needing deletion if it has run out of penetrations.
void Projectile::Explode(vector<Visual> &visuals, double intersection, Point hitVelocity)
{
clip = intersection;
distanceTraveled += velocity.Length() * intersection;
for(const auto &it : weapon->HitEffects())
for(int i = 0; i < it.second; ++i)
{
visuals.emplace_back(*it.first, position + velocity * intersection, velocity, angle, hitVelocity);
}
lifetime = -100;
if(++penetrations > weapon->Penetration())
{
clip = intersection;
lifetime = -100;
}
}


Expand All @@ -281,10 +284,19 @@ double Projectile::Clip() const



// Get whether the lifetime on this projectile has run out.
bool Projectile::IsDead() const
{
return lifetime < 0;
Amazinite marked this conversation as resolved.
Show resolved Hide resolved
}



// This projectile was killed, e.g. by an anti-missile system.
void Projectile::Kill()
{
lifetime = 0;
penetrations = weapon->Penetration();
}


Expand Down
7 changes: 5 additions & 2 deletions source/Projectile.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ class Projectile : public Body {
// Move the projectile. It may create effects or submunitions.
void Move(std::vector<Visual> &visuals, std::vector<Projectile> &projectiles);
// This projectile hit something. Create the explosion, if any. This also
// marks the projectile as needing deletion.
// marks the projectile as needing deletion if it has run out of penetrations.
void Explode(std::vector<Visual> &visuals, double intersection, Point hitVelocity = Point());
// Get the amount of clipping that should be applied when drawing this projectile.
double Clip() const;
// Get whether the lifetime on this projectile has run out.
bool IsDead() const;
// This projectile was killed, e.g. by an anti-missile system.
void Kill();

Expand Down Expand Up @@ -90,7 +92,8 @@ class Projectile : public Body {

double clip = 1.;
int lifetime = 0;
double distanceTraveled = 0;
double distanceTraveled = 0.;
int penetrations = 0;
Amazinite marked this conversation as resolved.
Show resolved Hide resolved
bool hasLock = true;
};

Expand Down
2 changes: 2 additions & 0 deletions source/Weapon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ void Weapon::LoadWeapon(const DataNode &node)
missileStrength = max(0., value);
else if(key == "anti-missile")
antiMissile = max(0., value);
else if(key == "penetration")
penetration = max(0., value);
else if(key == "velocity")
velocity = value;
else if(key == "random velocity")
Expand Down
Loading