source-engine/game/server/physics_impact_damage.cpp

743 lines
22 KiB
C++
Raw Normal View History

2020-04-22 16:56:21 +00:00
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//
//=============================================================================//
#include "cbase.h"
#include "physics_impact_damage.h"
#include "shareddefs.h"
#include "vphysics/friction.h"
#include "vphysics/player_controller.h"
#include "world.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
//==============================================================================================
// PLAYER PHYSICS DAMAGE TABLE
//==============================================================================================
static impactentry_t playerLinearTable[] =
{
{ 150*150, 5 },
{ 250*250, 10 },
{ 450*450, 20 },
{ 550*550, 50 },
{ 700*700, 100 },
{ 1000*1000, 500 },
};
static impactentry_t playerAngularTable[] =
{
{ 100*100, 10 },
{ 150*150, 20 },
{ 200*200, 50 },
{ 300*300, 500 },
};
impactdamagetable_t gDefaultPlayerImpactDamageTable =
{
playerLinearTable,
playerAngularTable,
ARRAYSIZE(playerLinearTable),
ARRAYSIZE(playerAngularTable),
24*24.0f, // minimum linear speed
360*360.0f, // minimum angular speed
2.0f, // can't take damage from anything under 2kg
5.0f, // anything less than 5kg is "small"
5.0f, // never take more than 5 pts of damage from anything under 5kg
36*36.0f, // <5kg objects must go faster than 36 in/s to do damage
0.0f, // large mass in kg (no large mass effects)
1.0f, // large mass scale
2.0f, // large mass falling scale
320.0f, // min velocity for player speed to cause damage
};
//==============================================================================================
// PLAYER-IN-VEHICLE PHYSICS DAMAGE TABLE
//==============================================================================================
static impactentry_t playerVehicleLinearTable[] =
{
{ 450*450, 5 },
{ 600*600, 10 },
{ 700*700, 25 },
{ 1000*1000, 50 },
{ 1500*1500, 100 },
{ 2000*2000, 500 },
};
static impactentry_t playerVehicleAngularTable[] =
{
{ 100*100, 10 },
{ 150*150, 20 },
{ 200*200, 50 },
{ 300*300, 500 },
};
impactdamagetable_t gDefaultPlayerVehicleImpactDamageTable =
{
playerVehicleLinearTable,
playerVehicleAngularTable,
ARRAYSIZE(playerVehicleLinearTable),
ARRAYSIZE(playerVehicleAngularTable),
24*24, // minimum linear speed
360*360, // minimum angular speed
80, // can't take damage from anything under 80 kg
150, // anything less than 150kg is "small"
5, // never take more than 5 pts of damage from anything under 150kg
36*36, // <150kg objects must go faster than 36 in/s to do damage
0, // large mass in kg (no large mass effects)
1.0f, // large mass scale
1.0f, // large mass falling scale
0.0f, // min vel
};
//==============================================================================================
// NPC PHYSICS DAMAGE TABLE
//==============================================================================================
static impactentry_t npcLinearTable[] =
{
{ 150*150, 5 },
{ 250*250, 10 },
{ 350*350, 50 },
{ 500*500, 100 },
{ 1000*1000, 500 },
};
static impactentry_t npcAngularTable[] =
{
{ 100*100, 10 },
{ 150*150, 25 },
{ 200*200, 50 },
{ 250*250, 500 },
};
impactdamagetable_t gDefaultNPCImpactDamageTable =
{
npcLinearTable,
npcAngularTable,
ARRAYSIZE(npcLinearTable),
ARRAYSIZE(npcAngularTable),
24*24, // minimum linear speed squared
360*360, // minimum angular speed squared (360 deg/s to cause spin/slice damage)
2, // can't take damage from anything under 2kg
5, // anything less than 5kg is "small"
5, // never take more than 5 pts of damage from anything under 5kg
36*36, // <5kg objects must go faster than 36 in/s to do damage
VPHYSICS_LARGE_OBJECT_MASS, // large mass in kg
4, // large mass scale (anything over 500kg does 4X as much energy to read from damage table)
5, // large mass falling scale (emphasize falling/crushing damage over sideways impacts since the stress will kill you anyway)
0.0f, // min vel
};
//==============================================================================================
// GLASS DAMAGE TABLE
//==============================================================================================
static impactentry_t glassLinearTable[] =
{
{ 25*25, 10 },
{ 50*50, 20 },
{ 100*100, 50 },
{ 200*200, 75 },
{ 500*500, 100 },
{ 250*250, 500 },
};
static impactentry_t glassAngularTable[] =
{
{ 50*50, 25 },
{ 100*100, 50 },
{ 200*200, 100 },
{ 250*250, 500 },
};
impactdamagetable_t gGlassImpactDamageTable =
{
glassLinearTable,
glassAngularTable,
ARRAYSIZE(glassLinearTable),
ARRAYSIZE(glassAngularTable),
8*8, // minimum linear speed squared
360*360, // minimum angular speed squared (360 deg/s to cause spin/slice damage)
2, // can't take damage from anything under 2kg
1, // anything less than 1kg is "small"
10, // never take more than 10 pts of damage from anything under 1kg
8*8, // <1kg objects must go faster than 8 in/s to do damage
50, // large mass in kg
4, // large mass scale (anything over 50kg does 4X as much energy to read from damage table)
0.0f, // min vel
};
//==============================================================================================
// PHYSICS TABLE NAMES
//==============================================================================================
struct damagetable_t
{
const char *pszTableName;
impactdamagetable_t *pTable;
};
static damagetable_t gDamageTableRegistry[] =
{
{
"player",
&gDefaultPlayerImpactDamageTable,
},
{
"player_vehicle",
&gDefaultPlayerVehicleImpactDamageTable,
},
{
"npc",
&gDefaultNPCImpactDamageTable,
},
{
"glass",
&gGlassImpactDamageTable,
},
};
//==============================================================================================
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
float ReadDamageTable( impactentry_t *pTable, int tableCount, float impulse, bool bDebug )
{
if ( pTable )
{
int i;
for ( i = 0; i < tableCount; i++ )
{
if ( impulse < pTable[i].impulse )
break;
}
if ( i > 0 )
{
i--;
if ( bDebug )
{
Msg("Damage %.0f, energy %.0f\n", pTable[i].damage, FastSqrt(impulse) );
}
return pTable[i].damage;
}
}
return 0;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
float CalculatePhysicsImpactDamage( int index, gamevcollisionevent_t *pEvent, const impactdamagetable_t &table, float energyScale, bool allowStaticDamage, int &damageType, bool bDamageFromHeldObjects )
{
damageType = DMG_CRUSH;
int otherIndex = !index;
// UNDONE: Expose a flag for self-inflicted damage? Can't think of a valid case so far.
if ( pEvent->pEntities[0] == pEvent->pEntities[1] )
return 0;
if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_NO_NPC_IMPACT_DMG )
{
if( pEvent->pEntities[index]->IsNPC() || pEvent->pEntities[index]->IsPlayer() )
{
return 0;
}
}
// use implicit velocities on ragdolls since they may have high constraint velocities that aren't actually executed, just pushed through contacts
if (( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_PART_OF_RAGDOLL) && pEvent->pEntities[index]->IsPlayer() )
{
pEvent->pObjects[otherIndex]->GetImplicitVelocity( &pEvent->preVelocity[otherIndex], &pEvent->preAngularVelocity[otherIndex] );
}
// Dissolving impact damage results in death always.
if ( ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_DMG_DISSOLVE ) &&
!pEvent->pEntities[index]->IsEFlagSet(EFL_NO_DISSOLVE) )
{
damageType |= DMG_DISSOLVE;
return 1000;
}
if ( energyScale <= 0.0f )
return 0;
const int gameFlagsNoDamage = FVPHYSICS_CONSTRAINT_STATIC | FVPHYSICS_NO_IMPACT_DMG;
// NOTE: Crushing damage is handled by stress calcs in vphysics update functions, this is ONLY impact damage
// this is a non-moving object due to a constraint - no damage
if ( pEvent->pObjects[otherIndex]->GetGameFlags() & gameFlagsNoDamage )
return 0;
// If it doesn't take damage from held objects and the object is being held - no damage
if ( !bDamageFromHeldObjects && ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_PLAYER_HELD ) )
{
// If it doesn't take damage from held objects - no damage
if ( !bDamageFromHeldObjects )
return 0;
}
if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_MULTIOBJECT_ENTITY )
{
// UNDONE: Add up mass here for car wheels and prop_ragdoll pieces?
IPhysicsObject *pList[VPHYSICS_MAX_OBJECT_LIST_COUNT];
int count = pEvent->pEntities[otherIndex]->VPhysicsGetObjectList( pList, ARRAYSIZE(pList) );
for ( int i = 0; i < count; i++ )
{
if ( pList[i]->GetGameFlags() & gameFlagsNoDamage )
return 0;
}
}
if ( pEvent->pObjects[index]->GetGameFlags() & FVPHYSICS_PLAYER_HELD )
{
// players can't damage held objects
if ( pEvent->pEntities[otherIndex]->IsPlayer() )
return 0;
allowStaticDamage = false;
}
#if 0
{
PhysGetDamageInflictorVelocityStartOfFrame( pEvent->pObjects[otherIndex], pEvent->preVelocity[otherIndex], pEvent->preAngularVelocity[otherIndex] );
}
#endif
float otherSpeedSqr = pEvent->preVelocity[otherIndex].LengthSqr();
float otherAngSqr = 0;
// factor in angular for sharp objects
if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_DMG_SLICE )
{
otherAngSqr = pEvent->preAngularVelocity[otherIndex].LengthSqr();
}
float otherMass = pEvent->pObjects[otherIndex]->GetMass();
if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_PLAYER_HELD )
{
if ( gpGlobals->maxClients == 1 )
{
// if the player is holding the object, use it's real mass (player holding reduced the mass)
CBasePlayer *pPlayer = UTIL_GetLocalPlayer();
if ( pPlayer )
{
otherMass = pPlayer->GetHeldObjectMass( pEvent->pObjects[otherIndex] );
}
}
}
// NOTE: sum the mass of each object in this system for the purpose of damage
if ( pEvent->pEntities[otherIndex] && (pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_MULTIOBJECT_ENTITY) )
{
otherMass = PhysGetEntityMass( pEvent->pEntities[otherIndex] );
}
if ( pEvent->pObjects[otherIndex]->GetGameFlags() & FVPHYSICS_HEAVY_OBJECT )
{
otherMass = table.largeMassMin;
if ( energyScale < 2.0f )
{
energyScale = 2.0f;
}
}
// UNDONE: allowStaticDamage is a hack - work out some method for
// breakable props to impact the world and break!!
if ( !allowStaticDamage )
{
if ( otherMass < table.minMass )
return 0;
// check to see if the object is small
if ( otherMass < table.smallMassMax && otherSpeedSqr < table.smallMassMinSpeedSqr )
return 0;
if ( otherSpeedSqr < table.minSpeedSqr && otherAngSqr < table.minRotSpeedSqr )
return 0;
}
// Add extra oomph for floating objects
if ( pEvent->pEntities[index]->IsFloating() && !pEvent->pEntities[otherIndex]->IsWorld() )
{
if ( energyScale < 3.0f )
{
energyScale = 3.0f;
}
}
float damage = 0;
bool bDebug = false;//(&table == &gDefaultPlayerImpactDamageTable);
// don't ever take spin damage from slowly spinning objects
if ( otherAngSqr > table.minRotSpeedSqr )
{
Vector otherInertia = pEvent->pObjects[otherIndex]->GetInertia();
float angularMom = DotProductAbs( otherInertia, pEvent->preAngularVelocity[otherIndex] );
damage = ReadDamageTable( table.angularTable, table.angularCount, angularMom * energyScale, bDebug );
if ( damage > 0 )
{
// Msg("Spin : %.1f, Damage %.0f\n", FastSqrt(angularMom), damage );
damageType |= DMG_SLASH;
}
}
float deltaV = pEvent->preVelocity[index].Length() - pEvent->postVelocity[index].Length();
float mass = pEvent->pObjects[index]->GetMass();
// If I lost speed, and I lost less than min velocity, then filter out this energy
if ( deltaV > 0 && deltaV < table.myMinVelocity )
{
deltaV = 0;
}
float eliminatedEnergy = deltaV * deltaV * mass;
deltaV = pEvent->preVelocity[otherIndex].Length() - pEvent->postVelocity[otherIndex].Length();
float otherEliminatedEnergy = deltaV * deltaV * otherMass;
// exaggerate the effects of really large objects
if ( otherMass >= table.largeMassMin )
{
otherEliminatedEnergy *= table.largeMassScale;
float dz = pEvent->preVelocity[otherIndex].z - pEvent->postVelocity[otherIndex].z;
if ( deltaV > 0 && dz < 0 && pEvent->preVelocity[otherIndex].z < 0 )
{
float factor = fabs(dz / deltaV);
otherEliminatedEnergy *= (1 + factor * (table.largeMassFallingScale - 1.0f));
}
}
eliminatedEnergy += otherEliminatedEnergy;
// now in units of this character's speed squared
float invMass = pEvent->pObjects[index]->GetInvMass();
if ( !pEvent->pObjects[index]->IsMoveable() )
{
// inv mass is zero, but impact damage is enabled on this
// prop, so recompute:
invMass = 1.0f / pEvent->pObjects[index]->GetMass();
}
else if ( pEvent->pObjects[index]->GetGameFlags() & FVPHYSICS_PLAYER_HELD )
{
if ( gpGlobals->maxClients == 1 )
{
// if the player is holding the object, use it's real mass (player holding reduced the mass)
CBasePlayer *pPlayer = UTIL_GetLocalPlayer();
if ( pPlayer )
{
float mass = pPlayer->GetHeldObjectMass( pEvent->pObjects[index] );
if ( mass > 0 )
{
invMass = 1.0f / mass;
}
}
}
}
eliminatedEnergy *= invMass * energyScale;
damage += ReadDamageTable( table.linearTable, table.linearCount, eliminatedEnergy, bDebug );
if ( !pEvent->pObjects[otherIndex]->IsStatic() && otherMass < table.smallMassMax && table.smallMassCap > 0 )
{
damage = clamp( damage, 0.f, table.smallMassCap );
}
return damage;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
float CalculateDefaultPhysicsDamage( int index, gamevcollisionevent_t *pEvent, float energyScale, bool allowStaticDamage, int &damageType, string_t iszDamageTableName, bool bDamageFromHeldObjects )
{
// If we have a specified damage table, find it and use it instead
if ( iszDamageTableName != NULL_STRING )
{
for ( int i = 0; i < ARRAYSIZE(gDamageTableRegistry); i++ )
{
if ( !Q_strcmp( gDamageTableRegistry[i].pszTableName, STRING(iszDamageTableName) ) )
return CalculatePhysicsImpactDamage( index, pEvent, *(gDamageTableRegistry[i].pTable), energyScale, allowStaticDamage, damageType, bDamageFromHeldObjects );
}
Warning("Failed to find custom physics damage table name: %s\n", STRING(iszDamageTableName) );
}
return CalculatePhysicsImpactDamage( index, pEvent, gDefaultNPCImpactDamageTable, energyScale, allowStaticDamage, damageType, bDamageFromHeldObjects );
}
static bool IsPhysicallyControlled( CBaseEntity *pEntity, IPhysicsObject *pPhysics )
{
bool isPhysical = false;
if ( pEntity->GetMoveType() == MOVETYPE_VPHYSICS )
{
isPhysical = true;
}
else
{
if ( pPhysics->GetShadowController() )
{
isPhysical = pPhysics->GetShadowController()->IsPhysicallyControlled();
}
}
return isPhysical;
}
float CalculateObjectStress( IPhysicsObject *pObject, CBaseEntity *pInputOwnerEntity, vphysics_objectstress_t *pOutput )
{
CUtlVector< CBaseEntity * > pObjectList;
CUtlVector< Vector > objectForce;
bool hasLargeObject = false;
// add a slot for static objects
pObjectList.AddToTail( NULL );
objectForce.AddToTail( vec3_origin );
// add a slot for friendly objects
pObjectList.AddToTail( NULL );
objectForce.AddToTail( vec3_origin );
CBaseCombatCharacter *pBCC = pInputOwnerEntity->MyCombatCharacterPointer();
IPhysicsFrictionSnapshot *pSnapshot = pObject->CreateFrictionSnapshot();
float objMass = pObject->GetMass();
while ( pSnapshot->IsValid() )
{
float force = pSnapshot->GetNormalForce();
if ( force > 0.0f )
{
IPhysicsObject *pOther = pSnapshot->GetObject(1);
CBaseEntity *pOtherEntity = static_cast<CBaseEntity *>(pOther->GetGameData());
if ( !pOtherEntity )
{
// object was just deleted, but we still have a contact point this frame...
// just assume it came from the world.
pOtherEntity = GetWorldEntity();
}
CBaseEntity *pOtherOwner = pOtherEntity;
if ( pOtherEntity->GetOwnerEntity() )
{
pOtherOwner = pOtherEntity->GetOwnerEntity();
}
int outIndex = 0;
if ( !pOther->IsMoveable() )
{
outIndex = 0;
}
// NavIgnored objects are often being pushed by a friendly
else if ( pBCC && (pBCC->IRelationType( pOtherOwner ) == D_LI || pOtherEntity->IsNavIgnored()) )
{
outIndex = 1;
}
// player held objects do no stress
else if ( pOther->GetGameFlags() & FVPHYSICS_PLAYER_HELD )
{
outIndex = 1;
}
else
{
if ( pOther->GetMass() >= VPHYSICS_LARGE_OBJECT_MASS )
{
if ( pInputOwnerEntity->GetGroundEntity() != pOtherEntity)
{
hasLargeObject = true;
}
}
// moveable, non-friendly
// aggregate contacts over each object to avoid greater stress in multiple contact cases
// NOTE: Contacts should be in order, so this shouldn't ever search, but just in case
outIndex = pObjectList.Count();
for ( int i = pObjectList.Count()-1; i >= 2; --i )
{
if ( pObjectList[i] == pOtherOwner )
{
outIndex = i;
break;
}
}
if ( outIndex == pObjectList.Count() )
{
pObjectList.AddToTail( pOtherOwner );
objectForce.AddToTail( vec3_origin );
}
}
if ( outIndex != 0 && pInputOwnerEntity->GetMoveType() != MOVETYPE_VPHYSICS && !IsPhysicallyControlled(pOtherEntity, pOther) )
{
// UNDONE: Test this! This is to remove any shadow/shadow stress. The game should handle this with blocked/damage
force = 0.0f;
}
Vector normal;
pSnapshot->GetSurfaceNormal( normal );
objectForce[outIndex] += normal * force;
}
pSnapshot->NextFrictionData();
}
pObject->DestroyFrictionSnapshot( pSnapshot );
pSnapshot = NULL;
// clear out all friendly force
objectForce[1].Init();
float sum = 0;
Vector negativeForce = vec3_origin;
Vector positiveForce = vec3_origin;
Assert( pObjectList.Count() == objectForce.Count() );
for ( int objectIndex = pObjectList.Count()-1; objectIndex >= 0; --objectIndex )
{
sum += objectForce[objectIndex].Length();
for ( int i = 0; i < 3; i++ )
{
if ( objectForce[objectIndex][i] < 0 )
{
negativeForce[i] -= objectForce[objectIndex][i];
}
else
{
positiveForce[i] += objectForce[objectIndex][i];
}
}
}
// "external" stress is two way (something pushes on the object and something else pushes back)
// so the set of minimum values per component are the projections of the two-way force
// "internal" stress is one way (the object is pushing against something OR something pushing back)
// the momentum must have come from inside the object (gravity, controller, etc)
Vector internalForce = vec3_origin;
Vector externalForce = vec3_origin;
for ( int i = 0; i < 3; i++ )
{
if ( negativeForce[i] < positiveForce[i] )
{
internalForce[i] = positiveForce[i] - negativeForce[i];
externalForce[i] = negativeForce[i];
}
else
{
internalForce[i] = negativeForce[i] - positiveForce[i];
externalForce[i] = positiveForce[i];
}
}
// sum is kg in / s
Vector gravVector;
physenv->GetGravity( &gravVector );
float gravity = gravVector.Length();
if ( pInputOwnerEntity->GetMoveType() != MOVETYPE_VPHYSICS && pObject->IsMoveable() )
{
Vector lastVel;
lastVel.Init();
if ( pObject->GetShadowController() )
{
pObject->GetShadowController()->GetLastImpulse( &lastVel );
}
else
{
if ( ( pObject->GetCallbackFlags() & CALLBACK_IS_PLAYER_CONTROLLER ) )
{
CBasePlayer *pPlayer = ToBasePlayer( pInputOwnerEntity );
IPhysicsPlayerController *pController = pPlayer ? pPlayer->GetPhysicsController() : NULL;
if ( pController )
{
pController->GetLastImpulse( &lastVel );
}
}
}
// Work in progress...
// Peek into the controller for this object. Look at the input velocity and make sure it's all
// accounted for in the computed stress. If not, redistribute external to internal as it's
// probably being reflected in a way we can't measure here.
float inputLen = lastVel.Length() * (1.0f / physenv->GetSimulationTimestep()) * objMass;
if ( inputLen > 0.0f )
{
float internalLen = internalForce.Length();
if ( internalLen < inputLen )
{
float ratio = internalLen / inputLen;
Vector delta = internalForce * (1.0f - ratio);
internalForce += delta;
float deltaLen = delta.Length();
sum -= deltaLen;
float extLen = VectorNormalize(externalForce) - deltaLen;
if ( extLen < 0 )
{
extLen = 0;
}
externalForce *= extLen;
}
}
}
float invGravity = gravity;
if ( invGravity <= 0 )
{
invGravity = 1.0f;
}
else
{
invGravity = 1.0f / invGravity;
}
sum *= invGravity;
internalForce *= invGravity;
externalForce *= invGravity;
if ( !pObject->IsMoveable() )
{
// the above algorithm will see almost all force as internal if the object is not moveable
// (it doesn't push on anything else, so nothing is reciprocated)
// exceptions for friction of a single other object with multiple contact points on this object
// But the game wants to see it all as external because obviously the object can't move, so it can't have
// internal stress
externalForce = internalForce;
internalForce.Init();
if ( !pObject->IsStatic() )
{
sum += objMass;
}
}
else
{
// assume object is at rest
if ( sum > objMass )
{
sum = objMass + (sum-objMass) * 0.5;
}
}
if ( pOutput )
{
pOutput->exertedStress = internalForce.Length();
pOutput->receivedStress = externalForce.Length();
pOutput->hasNonStaticStress = pObjectList.Count() > 2 ? true : false;
pOutput->hasLargeObjectContact = hasLargeObject;
}
// sum is now kg
return sum;
}