You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1225 lines
32 KiB
1225 lines
32 KiB
//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose: Implements breakables and pushables. func_breakable is a bmodel
|
|
// that breaks into pieces after taking damage.
|
|
//
|
|
//=============================================================================//
|
|
|
|
#include "cbase.h"
|
|
#include "player.h"
|
|
#include "filters.h"
|
|
#include "func_break.h"
|
|
#include "decals.h"
|
|
#include "explode.h"
|
|
#include "in_buttons.h"
|
|
#include "physics.h"
|
|
#include "IEffects.h"
|
|
#include "vstdlib/random.h"
|
|
#include "engine/IEngineSound.h"
|
|
#include "SoundEmitterSystem/isoundemittersystembase.h"
|
|
#include "globals.h"
|
|
#include "util.h"
|
|
#include "physics_impact_damage.h"
|
|
#include "tier0/icommandline.h"
|
|
|
|
#ifdef PORTAL
|
|
#include "prop_portal_shared.h"
|
|
#endif
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
ConVar func_break_max_pieces( "func_break_max_pieces", "15", FCVAR_ARCHIVE | FCVAR_REPLICATED );
|
|
ConVar func_break_reduction_factor( "func_break_reduction_factor", ".5" );
|
|
extern ConVar breakable_disable_gib_limit;
|
|
|
|
extern Vector g_vecAttackDir;
|
|
|
|
// Just add more items to the bottom of this array and they will automagically be supported
|
|
// This is done instead of just a classname in the FGD so we can control which entities can
|
|
// be spawned, and still remain fairly flexible
|
|
|
|
const char *CBreakable::pSpawnObjects[] =
|
|
{
|
|
NULL, // 0
|
|
"item_battery", // 1
|
|
"item_healthkit", // 2
|
|
"item_ammo_pistol", // 3
|
|
"item_ammo_pistol_large", // 4
|
|
"item_ammo_smg1", // 5
|
|
"item_ammo_smg1_large", // 6
|
|
"item_ammo_ar2", // 7
|
|
"item_ammo_ar2_large", // 8
|
|
"item_box_buckshot", // 9
|
|
"item_flare_round", // 10
|
|
"item_box_flare_rounds", // 11
|
|
"item_rpg_round", // 12
|
|
"unused (item_smg1_grenade) 13",// 13
|
|
"item_box_sniper_rounds", // 14
|
|
"unused (???"") 15", // 15 - split into two strings to avoid trigraph warning
|
|
"weapon_stunstick", // 16
|
|
"unused (weapon_ar1) 17", // 17
|
|
"weapon_ar2", // 18
|
|
"unused (???"") 19", // 19 - split into two strings to avoid trigraph warning
|
|
"weapon_rpg", // 20
|
|
"weapon_smg1", // 21
|
|
"unused (weapon_smg2) 22", // 22
|
|
"unused (weapon_slam) 23", // 23
|
|
"weapon_shotgun", // 24
|
|
"unused (weapon_molotov) 25",// 25
|
|
"item_dynamic_resupply", // 26
|
|
};
|
|
|
|
const char *pFGDPropData[] =
|
|
{
|
|
NULL,
|
|
"Wooden.Tiny",
|
|
"Wooden.Small",
|
|
"Wooden.Medium",
|
|
"Wooden.Large",
|
|
"Wooden.Huge",
|
|
"Metal.Small",
|
|
"Metal.Medium",
|
|
"Metal.Large",
|
|
"Cardboard.Small",
|
|
"Cardboard.Medium",
|
|
"Cardboard.Large",
|
|
"Stone.Small",
|
|
"Stone.Medium",
|
|
"Stone.Large",
|
|
"Stone.Huge",
|
|
"Glass.Small",
|
|
"Plastic.Small",
|
|
"Plastic.Medium",
|
|
"Plastic.Large",
|
|
"Pottery.Small",
|
|
"Pottery.Medium",
|
|
"Pottery.Large",
|
|
"Pottery.Huge",
|
|
"Glass.Window",
|
|
};
|
|
|
|
LINK_ENTITY_TO_CLASS( func_breakable, CBreakable );
|
|
BEGIN_DATADESC( CBreakable )
|
|
|
|
DEFINE_FIELD( m_Material, FIELD_INTEGER ),
|
|
DEFINE_KEYFIELD( m_Explosion, FIELD_INTEGER, "explosion" ),
|
|
DEFINE_KEYFIELD( m_GibDir, FIELD_VECTOR, "gibdir" ),
|
|
DEFINE_FIELD( m_hBreaker, FIELD_EHANDLE ),
|
|
|
|
// Don't need to save/restore these because we precache after restore
|
|
//DEFINE_FIELD( m_idShard, FIELD_INTEGER ),
|
|
DEFINE_FIELD( m_iszGibModel, FIELD_STRING ),
|
|
DEFINE_FIELD( m_iszSpawnObject, FIELD_STRING ),
|
|
DEFINE_KEYFIELD( m_ExplosionMagnitude, FIELD_INTEGER, "explodemagnitude" ),
|
|
DEFINE_KEYFIELD( m_flPressureDelay, FIELD_FLOAT, "PressureDelay" ),
|
|
DEFINE_KEYFIELD( m_iMinHealthDmg, FIELD_INTEGER, "minhealthdmg" ),
|
|
DEFINE_FIELD( m_bTookPhysicsDamage, FIELD_BOOLEAN ),
|
|
DEFINE_FIELD( m_iszPropData, FIELD_STRING ),
|
|
DEFINE_INPUT( m_impactEnergyScale, FIELD_FLOAT, "physdamagescale" ),
|
|
DEFINE_KEYFIELD( m_PerformanceMode, FIELD_INTEGER, "PerformanceMode" ),
|
|
|
|
DEFINE_INPUTFUNC( FIELD_VOID, "Break", InputBreak ),
|
|
DEFINE_INPUTFUNC( FIELD_INTEGER, "SetHealth", InputSetHealth ),
|
|
DEFINE_INPUTFUNC( FIELD_INTEGER, "AddHealth", InputAddHealth ),
|
|
DEFINE_INPUTFUNC( FIELD_INTEGER, "RemoveHealth", InputRemoveHealth ),
|
|
DEFINE_INPUTFUNC( FIELD_FLOAT, "SetMass", InputSetMass ),
|
|
|
|
// Function Pointers
|
|
DEFINE_ENTITYFUNC( BreakTouch ),
|
|
DEFINE_THINKFUNC( Die ),
|
|
|
|
// Outputs
|
|
DEFINE_OUTPUT(m_OnBreak, "OnBreak"),
|
|
DEFINE_OUTPUT(m_OnHealthChanged, "OnHealthChanged"),
|
|
|
|
DEFINE_FIELD( m_flDmgModBullet, FIELD_FLOAT ),
|
|
DEFINE_FIELD( m_flDmgModClub, FIELD_FLOAT ),
|
|
DEFINE_FIELD( m_flDmgModExplosive, FIELD_FLOAT ),
|
|
DEFINE_FIELD( m_flDmgModFire, FIELD_FLOAT ),
|
|
DEFINE_FIELD( m_iszPhysicsDamageTableName, FIELD_STRING ),
|
|
DEFINE_FIELD( m_iszBreakableModel, FIELD_STRING ),
|
|
DEFINE_FIELD( m_iBreakableSkin, FIELD_INTEGER ),
|
|
DEFINE_FIELD( m_iBreakableCount, FIELD_INTEGER ),
|
|
DEFINE_FIELD( m_iMaxBreakableSize, FIELD_INTEGER ),
|
|
DEFINE_FIELD( m_iszBasePropData, FIELD_STRING ),
|
|
DEFINE_FIELD( m_iInteractions, FIELD_INTEGER ),
|
|
DEFINE_FIELD( m_explodeRadius, FIELD_FLOAT ),
|
|
DEFINE_FIELD( m_iszModelName, FIELD_STRING ),
|
|
|
|
// Physics Influence
|
|
DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ),
|
|
DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ),
|
|
|
|
END_DATADESC()
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CBreakable::KeyValue( const char *szKeyName, const char *szValue )
|
|
{
|
|
// UNDONE_WC: explicitly ignoring these fields, but they shouldn't be in the map file!
|
|
if (FStrEq(szKeyName, "material"))
|
|
{
|
|
int i = atoi( szValue);
|
|
|
|
// 0:glass, 1:metal, 2:flesh, 3:wood
|
|
|
|
if ((i < 0) || (i >= matLastMaterial))
|
|
m_Material = matWood;
|
|
else
|
|
m_Material = (Materials)i;
|
|
}
|
|
else if (FStrEq(szKeyName, "deadmodel"))
|
|
{
|
|
}
|
|
else if (FStrEq(szKeyName, "shards"))
|
|
{
|
|
// m_iShards = atof(szValue);
|
|
}
|
|
else if (FStrEq(szKeyName, "gibmodel") )
|
|
{
|
|
m_iszGibModel = AllocPooledString(szValue);
|
|
}
|
|
else if (FStrEq(szKeyName, "spawnobject") )
|
|
{
|
|
int object = atoi( szValue );
|
|
if ( object > 0 && object < ARRAYSIZE(pSpawnObjects) )
|
|
m_iszSpawnObject = MAKE_STRING( pSpawnObjects[object] );
|
|
}
|
|
else if (FStrEq(szKeyName, "propdata") )
|
|
{
|
|
int pdata = atoi( szValue );
|
|
if ( pdata > 0 && pdata < ARRAYSIZE(pFGDPropData) )
|
|
{
|
|
m_iszPropData = MAKE_STRING( pFGDPropData[pdata] );
|
|
}
|
|
else if ( pdata )
|
|
{
|
|
// If you've hit this warning, it's probably because someone's added a new
|
|
// propdata field to func_breakables in the .fgd, and not added it to the
|
|
// pFGDPropData list.
|
|
Warning("func_breakable with invalid propdata %d.\n", pdata );
|
|
}
|
|
}
|
|
else if (FStrEq(szKeyName, "lip") )
|
|
{
|
|
}
|
|
else
|
|
return BaseClass::KeyValue( szKeyName, szValue );
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::Spawn( void )
|
|
{
|
|
// Initialize damage modifiers. Must be done before baseclass spawn.
|
|
m_flDmgModBullet = func_breakdmg_bullet.GetFloat();
|
|
m_flDmgModClub = func_breakdmg_club.GetFloat();
|
|
m_flDmgModExplosive = func_breakdmg_explosive.GetFloat();
|
|
m_flDmgModFire = 1.0f;
|
|
|
|
ParsePropData();
|
|
|
|
Precache( );
|
|
|
|
if ( !m_iHealth || FBitSet( m_spawnflags, SF_BREAK_TRIGGER_ONLY ) )
|
|
{
|
|
// This allows people to shoot at the glass (since it's penetrable)
|
|
if ( m_Material == matGlass )
|
|
{
|
|
m_iHealth = 1;
|
|
}
|
|
|
|
m_takedamage = DAMAGE_NO;
|
|
}
|
|
else
|
|
{
|
|
m_takedamage = DAMAGE_YES;
|
|
}
|
|
|
|
m_iMaxHealth = ( m_iHealth > 0 ) ? m_iHealth : 1;
|
|
|
|
SetSolid( SOLID_BSP );
|
|
SetMoveType( MOVETYPE_PUSH );
|
|
|
|
SetModel( STRING( GetModelName() ) );//set size and link into world.
|
|
|
|
SetTouch( &CBreakable::BreakTouch );
|
|
if ( FBitSet( m_spawnflags, SF_BREAK_TRIGGER_ONLY ) ) // Only break on trigger
|
|
{
|
|
SetTouch( NULL );
|
|
}
|
|
|
|
// Flag unbreakable glass as "worldbrush" so it will block ALL tracelines
|
|
if ( !IsBreakable() && m_nRenderMode != kRenderNormal )
|
|
AddFlag( FL_WORLDBRUSH );
|
|
|
|
if ( m_impactEnergyScale == 0 )
|
|
{
|
|
m_impactEnergyScale = 1.0;
|
|
}
|
|
|
|
CreateVPhysics();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Parse this prop's data, if it has a keyvalues section.
|
|
// Returns true only if this prop is using a model that has a prop_data section that's invalid.
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::ParsePropData( void )
|
|
{
|
|
if ( m_iszPropData == NULL_STRING )
|
|
return;
|
|
|
|
if ( StringHasPrefixCaseSensitive( STRING(m_iszPropData), "None" ) )
|
|
return;
|
|
|
|
g_PropDataSystem.ParsePropFromBase( this, this, STRING(m_iszPropData) );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
bool CBreakable::CreateVPhysics( void )
|
|
{
|
|
VPhysicsInitStatic();
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
const char *CBreakable::MaterialSound( Materials precacheMaterial )
|
|
{
|
|
switch ( precacheMaterial )
|
|
{
|
|
case matWood:
|
|
return "Breakable.MatWood";
|
|
case matFlesh:
|
|
return "Breakable.MatFlesh";
|
|
case matComputer:
|
|
return "Breakable.Computer";
|
|
case matUnbreakableGlass:
|
|
case matGlass:
|
|
return "Breakable.MatGlass";
|
|
case matMetalPanel:
|
|
case matMetal:
|
|
return "Breakable.MatMetal";
|
|
case matCinderBlock:
|
|
case matRocks:
|
|
return "Breakable.MatConcrete";
|
|
case matCeilingTile:
|
|
case matNone:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
void CBreakable::MaterialSoundRandom( int entindex, Materials soundMaterial, float volume )
|
|
{
|
|
const char *soundname;
|
|
soundname = MaterialSound( soundMaterial );
|
|
if ( !soundname )
|
|
return;
|
|
|
|
CSoundParameters params;
|
|
if ( !GetParametersForSound( soundname, params, NULL ) )
|
|
return;
|
|
|
|
CPASAttenuationFilter filter( CBaseEntity::Instance( entindex ), params.soundlevel );
|
|
|
|
|
|
EmitSound_t ep;
|
|
ep.m_nChannel = params.channel;
|
|
ep.m_pSoundName = params.soundname;
|
|
ep.m_flVolume = volume;
|
|
ep.m_SoundLevel = params.soundlevel;
|
|
|
|
EmitSound( filter, entindex, ep );
|
|
}
|
|
|
|
|
|
void CBreakable::Precache( void )
|
|
{
|
|
const char *pGibName = "WoodChunks";
|
|
|
|
switch (m_Material)
|
|
{
|
|
case matWood:
|
|
pGibName = "WoodChunks";
|
|
break;
|
|
|
|
case matUnbreakableGlass:
|
|
case matGlass:
|
|
pGibName = "GlassChunks";
|
|
break;
|
|
|
|
case matMetalPanel:
|
|
pGibName = "MetalPanelChunks";
|
|
break;
|
|
|
|
case matMetal:
|
|
pGibName = "MetalChunks";
|
|
break;
|
|
|
|
case matRocks:
|
|
pGibName = "ConcreteChunks";
|
|
break;
|
|
|
|
case matCinderBlock:
|
|
pGibName = "ConcreteChunks";
|
|
break;
|
|
|
|
#if HL2_EPISODIC
|
|
case matNone:
|
|
pGibName = "";
|
|
break;
|
|
#endif
|
|
|
|
default:
|
|
Warning("%s (%s) at (%.3f %.3f %.3f) using obsolete or unknown material type.\n", GetClassname(), GetDebugName(), GetAbsOrigin().x, GetAbsOrigin().y, GetAbsOrigin().z );
|
|
pGibName = "WoodChunks";
|
|
break;
|
|
}
|
|
|
|
if ( m_iszGibModel != NULL_STRING )
|
|
{
|
|
pGibName = STRING(m_iszGibModel);
|
|
}
|
|
|
|
m_iszModelName = MAKE_STRING( pGibName );
|
|
|
|
// Precache the spawn item's data
|
|
if ( !CommandLine()->CheckParm("-makereslists"))
|
|
{
|
|
if ( m_iszSpawnObject != NULL_STRING )
|
|
{
|
|
UTIL_PrecacheOther( STRING( m_iszSpawnObject ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Actually, precache all possible objects...
|
|
for ( int i = 0; i < ARRAYSIZE(pSpawnObjects) ; ++i )
|
|
{
|
|
if ( !pSpawnObjects[ i ] )
|
|
continue;
|
|
|
|
if ( !Q_strnicmp( pSpawnObjects[ i ], "unused", Q_strlen( "unused" ) ) )
|
|
continue;
|
|
|
|
UTIL_PrecacheOther( pSpawnObjects[ i ] );
|
|
}
|
|
}
|
|
|
|
PrecacheScriptSound( "Breakable.MatGlass" );
|
|
PrecacheScriptSound( "Breakable.MatWood" );
|
|
PrecacheScriptSound( "Breakable.MatMetal" );
|
|
PrecacheScriptSound( "Breakable.MatFlesh" );
|
|
PrecacheScriptSound( "Breakable.MatConcrete" );
|
|
PrecacheScriptSound( "Breakable.Computer" );
|
|
PrecacheScriptSound( "Breakable.Crate" );
|
|
PrecacheScriptSound( "Breakable.Glass" );
|
|
PrecacheScriptSound( "Breakable.Metal" );
|
|
PrecacheScriptSound( "Breakable.Flesh" );
|
|
PrecacheScriptSound( "Breakable.Concrete" );
|
|
PrecacheScriptSound( "Breakable.Ceiling" );
|
|
}
|
|
|
|
// play shard sound when func_breakable takes damage.
|
|
// the more damage, the louder the shard sound.
|
|
|
|
|
|
void CBreakable::DamageSound( void )
|
|
{
|
|
int pitch;
|
|
float fvol;
|
|
char *soundname = NULL;
|
|
int material = m_Material;
|
|
|
|
if (random->RandomInt(0,2))
|
|
{
|
|
pitch = PITCH_NORM;
|
|
}
|
|
else
|
|
{
|
|
pitch = 95 + random->RandomInt(0,34);
|
|
}
|
|
|
|
fvol = random->RandomFloat(0.75, 1.0);
|
|
|
|
if (material == matComputer && random->RandomInt(0,1))
|
|
{
|
|
material = matMetal;
|
|
}
|
|
|
|
switch (material)
|
|
{
|
|
case matGlass:
|
|
case matUnbreakableGlass:
|
|
soundname = "Breakable.MatGlass";
|
|
break;
|
|
|
|
case matWood:
|
|
soundname = "Breakable.MatWood";
|
|
break;
|
|
|
|
case matMetalPanel:
|
|
case matMetal:
|
|
soundname = "Breakable.MatMetal";
|
|
break;
|
|
|
|
case matRocks:
|
|
case matCinderBlock:
|
|
soundname = "Breakable.MatConcrete";
|
|
break;
|
|
|
|
case matComputer:
|
|
soundname = "Breakable.Computer";
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if ( soundname )
|
|
{
|
|
CSoundParameters params;
|
|
if ( GetParametersForSound( soundname, params, NULL ) )
|
|
{
|
|
CPASAttenuationFilter filter( this );
|
|
|
|
EmitSound_t ep;
|
|
ep.m_nChannel = params.channel;
|
|
ep.m_pSoundName = params.soundname;
|
|
ep.m_flVolume = fvol;
|
|
ep.m_SoundLevel = params.soundlevel;
|
|
ep.m_nPitch = pitch;
|
|
|
|
EmitSound( filter, entindex(), ep );
|
|
}
|
|
}
|
|
}
|
|
|
|
void CBreakable::BreakTouch( CBaseEntity *pOther )
|
|
{
|
|
float flDamage;
|
|
|
|
// only players can break these right now
|
|
if ( !pOther->IsPlayer() || !IsBreakable() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// can I be broken when run into?
|
|
if ( HasSpawnFlags( SF_BREAK_TOUCH ) )
|
|
{
|
|
flDamage = pOther->GetSmoothedVelocity().Length() * 0.01;
|
|
|
|
if (flDamage >= m_iHealth)
|
|
{
|
|
m_takedamage = DAMAGE_YES;
|
|
|
|
SetTouch( NULL );
|
|
OnTakeDamage( CTakeDamageInfo( pOther, pOther, flDamage, DMG_CRUSH ) );
|
|
|
|
// do a little damage to player if we broke glass or computer
|
|
CTakeDamageInfo info( pOther, pOther, flDamage/4, DMG_SLASH );
|
|
CalculateMeleeDamageForce( &info, (pOther->GetAbsOrigin() - GetAbsOrigin()), GetAbsOrigin() );
|
|
pOther->TakeDamage( info );
|
|
}
|
|
}
|
|
|
|
// can I be broken when stood upon?
|
|
if ( HasSpawnFlags( SF_BREAK_PRESSURE ) && pOther->GetGroundEntity() == this )
|
|
{
|
|
// play creaking sound here.
|
|
DamageSound();
|
|
|
|
m_hBreaker = pOther;
|
|
|
|
SetThink ( &CBreakable::Die );
|
|
SetTouch( NULL );
|
|
|
|
// Add optional delay
|
|
SetNextThink( gpGlobals->curtime + m_flPressureDelay );
|
|
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Input handler for adding to the breakable's health.
|
|
// Input : Integer health points to add.
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::InputAddHealth( inputdata_t &inputdata )
|
|
{
|
|
UpdateHealth( m_iHealth + inputdata.value.Int(), inputdata.pActivator );
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Input handler for breaking the breakable immediately.
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::InputBreak( inputdata_t &inputdata )
|
|
{
|
|
Break( inputdata.pActivator );
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Input handler for removing health from the breakable.
|
|
// Input : Integer health points to remove.
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::InputRemoveHealth( inputdata_t &inputdata )
|
|
{
|
|
UpdateHealth( m_iHealth - inputdata.value.Int(), inputdata.pActivator );
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Input handler for setting the breakable's health.
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::InputSetHealth( inputdata_t &inputdata )
|
|
{
|
|
UpdateHealth( inputdata.value.Int(), inputdata.pActivator );
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Input handler for setting the breakable's mass.
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::InputSetMass( inputdata_t &inputdata )
|
|
{
|
|
IPhysicsObject * vPhys = VPhysicsGetObject();
|
|
if ( vPhys )
|
|
{
|
|
float toMass = inputdata.value.Float();
|
|
Assert(toMass > 0);
|
|
vPhys->SetMass( toMass );
|
|
}
|
|
else
|
|
{
|
|
Warning( "Tried to call SetMass() on %s but it has no physics.\n", GetEntityName().ToCStr() );
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Choke point for changes to breakable health. Ensures outputs are fired.
|
|
// Input : iNewHealth -
|
|
// pActivator -
|
|
// Output : Returns true if the breakable survived, false if it died (broke).
|
|
//-----------------------------------------------------------------------------
|
|
bool CBreakable::UpdateHealth( int iNewHealth, CBaseEntity *pActivator )
|
|
{
|
|
if ( iNewHealth != m_iHealth )
|
|
{
|
|
m_iHealth = iNewHealth;
|
|
|
|
if ( m_iMaxHealth == 0 )
|
|
{
|
|
Assert( false );
|
|
m_iMaxHealth = 1;
|
|
}
|
|
|
|
// Output the new health as a percentage of max health [0..1]
|
|
float flRatio = clamp( (float)m_iHealth / (float)m_iMaxHealth, 0, 1 );
|
|
m_OnHealthChanged.Set( flRatio, pActivator, this );
|
|
|
|
if ( m_iHealth <= 0 )
|
|
{
|
|
Break( pActivator );
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if ( FBitSet( m_spawnflags, SF_BREAK_TRIGGER_ONLY ) )
|
|
{
|
|
m_takedamage = DAMAGE_NO;
|
|
}
|
|
else
|
|
{
|
|
m_takedamage = DAMAGE_YES;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Breaks the breakable if it can be broken.
|
|
// Input : pBreaker - The entity that caused us to break, either via an input,
|
|
// by shooting us, or by touching us.
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::Break( CBaseEntity *pBreaker )
|
|
{
|
|
if ( IsBreakable() )
|
|
{
|
|
m_hBreaker = pBreaker;
|
|
Die();
|
|
}
|
|
}
|
|
|
|
|
|
void CBreakable::TraceAttack( const CTakeDamageInfo &info, const Vector &vecDir, trace_t *ptr )
|
|
{
|
|
// random spark if this is a 'computer' object
|
|
if (random->RandomInt(0,1) )
|
|
{
|
|
switch( m_Material )
|
|
{
|
|
case matComputer:
|
|
{
|
|
g_pEffects->Sparks( ptr->endpos );
|
|
|
|
EmitSound( "Breakable.Computer" );
|
|
}
|
|
break;
|
|
|
|
case matUnbreakableGlass:
|
|
g_pEffects->Ricochet( ptr->endpos, (vecDir*-1.0f) );
|
|
break;
|
|
}
|
|
}
|
|
|
|
BaseClass::TraceAttack( info, vecDir, ptr );
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Allows us to take damage from physics objects
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent )
|
|
{
|
|
BaseClass::VPhysicsCollision( index, pEvent );
|
|
|
|
Vector damagePos;
|
|
pEvent->pInternalData->GetContactPoint( damagePos );
|
|
Vector damageForce = pEvent->postVelocity[index] * pEvent->pObjects[index]->GetMass();
|
|
if ( damageForce == vec3_origin )
|
|
{
|
|
// This can happen if this entity is a func_breakable, and can't move.
|
|
// Use the velocity of the entity that hit us instead.
|
|
damageForce = pEvent->postVelocity[!index] * pEvent->pObjects[!index]->GetMass();
|
|
}
|
|
|
|
// If we're supposed to explode on collision, do so
|
|
if ( HasSpawnFlags( SF_BREAK_PHYSICS_BREAK_IMMEDIATELY ) )
|
|
{
|
|
// We're toast
|
|
m_bTookPhysicsDamage = true;
|
|
CBaseEntity *pHitEntity = pEvent->pEntities[!index];
|
|
|
|
// HACKHACK: Reset mass to get correct collision response for the object breaking this glass
|
|
if ( m_Material == matGlass )
|
|
{
|
|
pEvent->pObjects[index]->SetMass( 2.0f );
|
|
}
|
|
CTakeDamageInfo dmgInfo( pHitEntity, pHitEntity, damageForce, damagePos, (m_iHealth + 1), DMG_CRUSH );
|
|
PhysCallbackDamage( this, dmgInfo, *pEvent, index );
|
|
}
|
|
else if ( !HasSpawnFlags( SF_BREAK_DONT_TAKE_PHYSICS_DAMAGE ) )
|
|
{
|
|
int otherIndex = !index;
|
|
CBaseEntity *pOther = pEvent->pEntities[otherIndex];
|
|
|
|
// We're to take normal damage from this
|
|
int damageType;
|
|
IBreakableWithPropData *pBreakableInterface = assert_cast<IBreakableWithPropData*>(this);
|
|
float damage = CalculateDefaultPhysicsDamage( index, pEvent, m_impactEnergyScale, true, damageType, pBreakableInterface->GetPhysicsDamageTable() );
|
|
if ( damage > 0 )
|
|
{
|
|
// HACKHACK: Reset mass to get correct collision response for the object breaking this glass
|
|
if ( m_Material == matGlass )
|
|
{
|
|
pEvent->pObjects[index]->SetMass( 2.0f );
|
|
}
|
|
CTakeDamageInfo dmgInfo( pOther, pOther, damageForce, damagePos, damage, damageType );
|
|
PhysCallbackDamage( this, dmgInfo, *pEvent, index );
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Allows us to make damage exceptions that are breakable-specific.
|
|
//-----------------------------------------------------------------------------
|
|
int CBreakable::OnTakeDamage( const CTakeDamageInfo &info )
|
|
{
|
|
Vector vecTemp;
|
|
|
|
CTakeDamageInfo subInfo = info;
|
|
|
|
// If attacker can't do at least the min required damage to us, don't take any damage from them
|
|
if ( m_takedamage == DAMAGE_NO || info.GetDamage() < m_iMinHealthDmg )
|
|
return 0;
|
|
|
|
// Check our damage filter
|
|
if ( !PassesDamageFilter(subInfo) )
|
|
{
|
|
m_bTookPhysicsDamage = false;
|
|
return 1;
|
|
}
|
|
|
|
vecTemp = subInfo.GetInflictor()->GetAbsOrigin() - WorldSpaceCenter();
|
|
|
|
if (!IsBreakable())
|
|
return 0;
|
|
|
|
float flPropDamage = GetBreakableDamage( subInfo, this );
|
|
subInfo.SetDamage( flPropDamage );
|
|
|
|
int iPrevHealth = m_iHealth;
|
|
BaseClass::OnTakeDamage( subInfo );
|
|
|
|
// HACK: slam health back to what it was so UpdateHealth can do its thing
|
|
int iNewHealth = m_iHealth;
|
|
m_iHealth = iPrevHealth;
|
|
if ( !UpdateHealth( iNewHealth, info.GetAttacker() ) )
|
|
return 1;
|
|
|
|
// Make a shard noise each time func breakable is hit, if it's capable of taking damage and it took damage
|
|
if ( m_takedamage == DAMAGE_YES && m_iHealth < iPrevHealth )
|
|
{
|
|
// Don't play shard noise if being burned.
|
|
// Don't play shard noise if cbreakable actually died.
|
|
if ( ( subInfo.GetDamageType() & DMG_BURN ) == false )
|
|
{
|
|
DamageSound();
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Purpose : Reset the OnGround flags for any entities that may have been
|
|
// resting on me
|
|
// Input :
|
|
// Output :
|
|
//------------------------------------------------------------------------------
|
|
void CBreakable::ResetOnGroundFlags(void)
|
|
{
|
|
// !!! HACK This should work!
|
|
// Build a box above the entity that looks like an 9 inch high sheet
|
|
Vector mins, maxs;
|
|
CollisionProp()->WorldSpaceAABB( &mins, &maxs );
|
|
mins.z -= 1;
|
|
maxs.z += 8;
|
|
|
|
// BUGBUG -- can only find 256 entities on a breakable -- should be enough
|
|
CBaseEntity *pList[256];
|
|
int count = UTIL_EntitiesInBox( pList, 256, mins, maxs, FL_ONGROUND );
|
|
if ( count )
|
|
{
|
|
for ( int i = 0; i < count; i++ )
|
|
{
|
|
pList[i]->SetGroundEntity( (CBaseEntity *)NULL );
|
|
}
|
|
}
|
|
|
|
#ifdef PORTAL
|
|
// !!! HACK This should work!
|
|
// Tell touching portals to fizzle
|
|
int iPortalCount = CProp_Portal_Shared::AllPortals.Count();
|
|
if( iPortalCount != 0 )
|
|
{
|
|
Vector vMin, vMax;
|
|
CollisionProp()->WorldSpaceAABB( &vMin, &vMax );
|
|
|
|
Vector vBoxCenter = ( vMin + vMax ) * 0.5f;
|
|
Vector vBoxExtents = ( vMax - vMin ) * 0.5f;
|
|
|
|
CProp_Portal **pPortals = CProp_Portal_Shared::AllPortals.Base();
|
|
for( int i = 0; i != iPortalCount; ++i )
|
|
{
|
|
CProp_Portal *pTempPortal = pPortals[i];
|
|
if( UTIL_IsBoxIntersectingPortal( vBoxCenter, vBoxExtents, pTempPortal ) )
|
|
{
|
|
pTempPortal->DoFizzleEffect( PORTAL_FIZZLE_KILLED, false );
|
|
pTempPortal->Fizzle();
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Breaks the breakable. m_hBreaker is the entity that caused us to break.
|
|
//-----------------------------------------------------------------------------
|
|
void CBreakable::Die( void )
|
|
{
|
|
Vector vecVelocity;// shard velocity
|
|
char cFlag = 0;
|
|
int pitch;
|
|
float fvol;
|
|
|
|
pitch = 95 + random->RandomInt(0,29);
|
|
|
|
if (pitch > 97 && pitch < 103)
|
|
{
|
|
pitch = 100;
|
|
}
|
|
|
|
// The more negative m_iHealth, the louder
|
|
// the sound should be.
|
|
|
|
fvol = random->RandomFloat(0.85, 1.0) + (abs(m_iHealth.Get()) / 100.0);
|
|
if (fvol > 1.0)
|
|
{
|
|
fvol = 1.0;
|
|
}
|
|
|
|
const char *soundname = NULL;
|
|
|
|
switch (m_Material)
|
|
{
|
|
default:
|
|
break;
|
|
|
|
case matGlass:
|
|
soundname = "Breakable.Glass";
|
|
cFlag = BREAK_GLASS;
|
|
break;
|
|
|
|
case matWood:
|
|
soundname = "Breakable.Crate";
|
|
cFlag = BREAK_WOOD;
|
|
break;
|
|
|
|
case matComputer:
|
|
soundname = "Breakable.Computer";
|
|
cFlag = BREAK_METAL;
|
|
break;
|
|
|
|
case matMetal:
|
|
case matMetalPanel:
|
|
soundname = "Breakable.Metal";
|
|
cFlag = BREAK_METAL;
|
|
break;
|
|
|
|
case matFlesh:
|
|
soundname = "Breakable.Flesh";
|
|
cFlag = BREAK_FLESH;
|
|
break;
|
|
|
|
case matRocks:
|
|
case matCinderBlock:
|
|
soundname = "Breakable.Concrete";
|
|
cFlag = BREAK_CONCRETE;
|
|
break;
|
|
|
|
case matCeilingTile:
|
|
soundname = "Breakable.Ceiling";
|
|
break;
|
|
}
|
|
|
|
if ( soundname )
|
|
{
|
|
if ( m_hBreaker && m_hBreaker->IsPlayer() )
|
|
{
|
|
IGameEvent * event = gameeventmanager->CreateEvent( "break_breakable" );
|
|
if ( event )
|
|
{
|
|
event->SetInt( "userid", ToBasePlayer( m_hBreaker )->GetUserID() );
|
|
event->SetInt( "entindex", entindex() );
|
|
event->SetInt( "material", cFlag );
|
|
gameeventmanager->FireEvent( event );
|
|
}
|
|
}
|
|
|
|
CSoundParameters params;
|
|
if ( GetParametersForSound( soundname, params, NULL ) )
|
|
{
|
|
CPASAttenuationFilter filter( this );
|
|
|
|
EmitSound_t ep;
|
|
ep.m_nChannel = params.channel;
|
|
ep.m_pSoundName = params.soundname;
|
|
ep.m_flVolume = fvol;
|
|
ep.m_SoundLevel = params.soundlevel;
|
|
ep.m_nPitch = pitch;
|
|
|
|
EmitSound( filter, entindex(), ep );
|
|
}
|
|
}
|
|
|
|
switch( m_Explosion )
|
|
{
|
|
case expDirected:
|
|
vecVelocity = g_vecAttackDir * -200;
|
|
break;
|
|
|
|
case expUsePrecise:
|
|
{
|
|
AngleVectors( m_GibDir, &vecVelocity, NULL, NULL );
|
|
vecVelocity *= 200;
|
|
}
|
|
break;
|
|
|
|
case expRandom:
|
|
vecVelocity.x = 0;
|
|
vecVelocity.y = 0;
|
|
vecVelocity.z = 0;
|
|
break;
|
|
|
|
default:
|
|
DevMsg("**ERROR - Unspecified gib dir method in func_breakable!\n");
|
|
break;
|
|
}
|
|
|
|
Vector vecSpot = WorldSpaceCenter();
|
|
CPVSFilter filter2( vecSpot );
|
|
|
|
int iModelIndex = 0;
|
|
CCollisionProperty *pCollisionProp = CollisionProp();
|
|
|
|
Vector vSize = pCollisionProp->OBBSize();
|
|
int iCount = ( vSize[0] * vSize[1] + vSize[1] * vSize[2] + vSize[2] * vSize[0] ) / ( 3 * 12 * 12 );
|
|
|
|
if ( iCount > func_break_max_pieces.GetInt() )
|
|
{
|
|
iCount = func_break_max_pieces.GetInt();
|
|
}
|
|
|
|
if ( !breakable_disable_gib_limit.GetBool() && iCount )
|
|
{
|
|
if ( m_PerformanceMode == PM_NO_GIBS )
|
|
{
|
|
iCount = 0;
|
|
}
|
|
else if ( m_PerformanceMode == PM_REDUCED_GIBS )
|
|
{
|
|
int iNewCount = iCount * func_break_reduction_factor.GetFloat();
|
|
iCount = MAX( iNewCount, 1 );
|
|
}
|
|
}
|
|
|
|
if ( m_iszModelName != NULL_STRING )
|
|
{
|
|
for ( int i = 0; i < iCount; i++ )
|
|
{
|
|
iModelIndex = modelinfo->GetModelIndex( g_PropDataSystem.GetRandomChunkModel( STRING( m_iszModelName ) ) );
|
|
|
|
// All objects except the first one in this run are marked as slaves...
|
|
int slaveFlag = 0;
|
|
if ( i != 0 )
|
|
{
|
|
slaveFlag = BREAK_SLAVE;
|
|
}
|
|
|
|
te->BreakModel( filter2, 0.0,
|
|
vecSpot, pCollisionProp->GetCollisionAngles(), vSize,
|
|
vecVelocity, iModelIndex, 100, 1, 2.5, cFlag | slaveFlag );
|
|
}
|
|
}
|
|
|
|
ResetOnGroundFlags();
|
|
|
|
// Don't fire something that could fire myself
|
|
SetName( NULL_STRING );
|
|
|
|
AddSolidFlags( FSOLID_NOT_SOLID );
|
|
|
|
// Fire targets on break
|
|
m_OnBreak.FireOutput( m_hBreaker, this );
|
|
|
|
VPhysicsDestroyObject();
|
|
SetThink( &CBreakable::SUB_Remove );
|
|
SetNextThink( gpGlobals->curtime + 0.1f );
|
|
if ( m_iszSpawnObject != NULL_STRING )
|
|
{
|
|
CBaseEntity::Create( STRING(m_iszSpawnObject), vecSpot, pCollisionProp->GetCollisionAngles(), this );
|
|
}
|
|
|
|
if ( Explodable() )
|
|
{
|
|
ExplosionCreate( vecSpot, pCollisionProp->GetCollisionAngles(), this, GetExplosiveDamage(), GetExplosiveRadius(), true );
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Returns whether this object can be broken.
|
|
//-----------------------------------------------------------------------------
|
|
bool CBreakable::IsBreakable( void )
|
|
{
|
|
return m_Material != matUnbreakableGlass;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
char const *CBreakable::DamageDecal( int bitsDamageType, int gameMaterial )
|
|
{
|
|
if ( m_Material == matGlass )
|
|
return "GlassBreak";
|
|
|
|
if ( m_Material == matUnbreakableGlass )
|
|
return "BulletProof";
|
|
|
|
return BaseClass::DamageDecal( bitsDamageType, gameMaterial );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Draw any debug text overlays
|
|
// Output : Current text offset from the top
|
|
//-----------------------------------------------------------------------------
|
|
int CBreakable::DrawDebugTextOverlays(void)
|
|
{
|
|
int text_offset = BaseClass::DrawDebugTextOverlays();
|
|
|
|
if (m_debugOverlays & OVERLAY_TEXT_BIT)
|
|
{
|
|
if ( GetMaxHealth() )
|
|
{
|
|
char tempstr[512];
|
|
Q_snprintf(tempstr,sizeof(tempstr),"Health: %i",GetHealth());
|
|
EntityText(text_offset,tempstr,0);
|
|
text_offset++;
|
|
}
|
|
|
|
if ( m_iszBasePropData != NULL_STRING )
|
|
{
|
|
char tempstr[512];
|
|
Q_snprintf(tempstr, sizeof(tempstr),"Base PropData: %s", STRING(m_iszBasePropData) );
|
|
EntityText( text_offset, tempstr, 0);
|
|
text_offset++;
|
|
}
|
|
}
|
|
|
|
return text_offset;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Keep track of physgun influence
|
|
//-----------------------------------------------------------------------------
|
|
|
|
void CBreakable::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason )
|
|
{
|
|
m_hPhysicsAttacker = pPhysGunUser;
|
|
m_flLastPhysicsInfluenceTime = gpGlobals->curtime;
|
|
}
|
|
|
|
void CBreakable::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t reason )
|
|
{
|
|
m_hPhysicsAttacker = pPhysGunUser;
|
|
m_flLastPhysicsInfluenceTime = gpGlobals->curtime;
|
|
}
|
|
|
|
CBasePlayer *CBreakable::HasPhysicsAttacker( float dt )
|
|
{
|
|
if (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime)
|
|
{
|
|
return m_hPhysicsAttacker;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
//=============================================================================================================================
|
|
// PUSHABLE
|
|
//=============================================================================================================================
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
class CPushable : public CBreakable
|
|
{
|
|
public:
|
|
DECLARE_CLASS( CPushable, CBreakable );
|
|
|
|
void Spawn ( void );
|
|
bool CreateVPhysics( void );
|
|
void Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value );
|
|
|
|
virtual int ObjectCaps( void ) { return BaseClass::ObjectCaps() | FCAP_ONOFF_USE; }
|
|
|
|
// breakables use an overridden takedamage
|
|
virtual int OnTakeDamage( const CTakeDamageInfo &info );
|
|
virtual void VPhysicsCollision( int index, gamevcollisionevent_t *pEvent );
|
|
unsigned int PhysicsSolidMaskForEntity( void ) const { return MASK_PLAYERSOLID; }
|
|
};
|
|
|
|
|
|
LINK_ENTITY_TO_CLASS( func_pushable, CPushable );
|
|
|
|
|
|
void CPushable::Spawn( void )
|
|
{
|
|
if ( HasSpawnFlags( SF_PUSH_BREAKABLE ) )
|
|
{
|
|
BaseClass::Spawn();
|
|
}
|
|
else
|
|
{
|
|
Precache();
|
|
|
|
SetSolid( SOLID_VPHYSICS );
|
|
|
|
SetMoveType( MOVETYPE_PUSH );
|
|
SetModel( STRING( GetModelName() ) );
|
|
|
|
CreateVPhysics();
|
|
}
|
|
}
|
|
|
|
|
|
bool CPushable::CreateVPhysics( void )
|
|
{
|
|
VPhysicsInitNormal( SOLID_VPHYSICS, 0, false );
|
|
IPhysicsObject *pPhysObj = VPhysicsGetObject();
|
|
if ( pPhysObj )
|
|
{
|
|
pPhysObj->SetMass( 30 );
|
|
// Vector vecInertia = Vector(800, 800, 800);
|
|
// pPhysObj->SetInertia( vecInertia );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Pull the func_pushable
|
|
void CPushable::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
|
|
{
|
|
BaseClass::Use( pActivator, pCaller, useType, value );
|
|
}
|
|
|
|
|
|
int CPushable::OnTakeDamage( const CTakeDamageInfo &info )
|
|
{
|
|
if ( m_spawnflags & SF_PUSH_BREAKABLE )
|
|
return BaseClass::OnTakeDamage( info );
|
|
|
|
return 1;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Allows us to take damage from physics objects
|
|
//-----------------------------------------------------------------------------
|
|
void CPushable::VPhysicsCollision( int index, gamevcollisionevent_t *pEvent )
|
|
{
|
|
int otherIndex = !index;
|
|
CBaseEntity *pOther = pEvent->pEntities[otherIndex];
|
|
if ( pOther->IsPlayer() )
|
|
{
|
|
// Pushables don't take damage from impacts with the player
|
|
// We call all the way back to the baseclass to get the physics effects.
|
|
CBaseEntity::VPhysicsCollision( index, pEvent );
|
|
return;
|
|
}
|
|
|
|
BaseClass::VPhysicsCollision( index, pEvent );
|
|
}
|
|
|