//========= 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(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; }