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.
1284 lines
32 KiB
1284 lines
32 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
//=============================================================================// |
|
|
|
#include "cbase.h" |
|
#include "soundenvelope.h" |
|
#include "Sprite.h" |
|
#include "entitylist.h" |
|
#include "ai_basenpc.h" |
|
#include "soundent.h" |
|
#include "explode.h" |
|
#include "physics.h" |
|
#include "physics_saverestore.h" |
|
#include "combine_mine.h" |
|
#include "movevars_shared.h" |
|
#include "vphysics/constraints.h" |
|
#include "ai_hint.h" |
|
|
|
enum |
|
{ |
|
MINE_STATE_DORMANT = 0, |
|
MINE_STATE_DEPLOY, // Try to lock down and arm |
|
MINE_STATE_CAPTIVE, // Held in the physgun |
|
MINE_STATE_ARMED, // Locked down and looking for targets |
|
MINE_STATE_TRIGGERED, // No turning back. I'm going to explode when I touch something. |
|
MINE_STATE_LAUNCHED, // Similar. Thrown from physgun. |
|
}; |
|
|
|
// for the Modification keyfield |
|
enum |
|
{ |
|
MINE_MODIFICATION_NORMAL = 0, |
|
MINE_MODIFICATION_CAVERN, |
|
}; |
|
|
|
// the citizen modified skins for the mine (inclusive): |
|
#define MINE_CITIZEN_SKIN_MIN 1 |
|
#define MINE_CITIZEN_SKIN_MAX 2 |
|
|
|
char *pszMineStateNames[] = |
|
{ |
|
"Dormant", |
|
"Deploy", |
|
"Captive", |
|
"Armed", |
|
"Triggered", |
|
"Launched", |
|
}; |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
// After this many flips, seriously cut the frequency with which you try. |
|
#define BOUNCEBOMB_MAX_FLIPS 5 |
|
|
|
// Approximate radius of the bomb's model |
|
#define BOUNCEBOMB_RADIUS 24 |
|
|
|
BEGIN_DATADESC( CBounceBomb ) |
|
DEFINE_THINKFUNC( ExplodeThink ), |
|
DEFINE_ENTITYFUNC( ExplodeTouch ), |
|
DEFINE_THINKFUNC( SearchThink ), |
|
DEFINE_THINKFUNC( BounceThink ), |
|
DEFINE_THINKFUNC( SettleThink ), |
|
DEFINE_THINKFUNC( CaptiveThink ), |
|
DEFINE_THINKFUNC( CavernBounceThink ), |
|
|
|
DEFINE_SOUNDPATCH( m_pWarnSound ), |
|
|
|
DEFINE_KEYFIELD( m_flExplosionDelay, FIELD_FLOAT, "ExplosionDelay" ), |
|
DEFINE_KEYFIELD( m_bBounce, FIELD_BOOLEAN, "Bounce" ), |
|
|
|
DEFINE_FIELD( m_bAwake, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_hNearestNPC, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_hSprite, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_LastSpriteColor, FIELD_COLOR32 ), |
|
|
|
DEFINE_FIELD( m_flHookPositions, FIELD_FLOAT ), |
|
DEFINE_FIELD( m_iHookN, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_iHookE, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_iHookS, FIELD_INTEGER ), |
|
DEFINE_FIELD( m_iAllHooks, FIELD_INTEGER ), |
|
|
|
DEFINE_KEYFIELD( m_bLockSilently, FIELD_BOOLEAN, "LockSilently" ), |
|
DEFINE_FIELD( m_bFoeNearest, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_flIgnoreWorldTime, FIELD_TIME ), |
|
DEFINE_KEYFIELD( m_bDisarmed, FIELD_BOOLEAN, "StartDisarmed" ), |
|
DEFINE_KEYFIELD( m_iModification, FIELD_INTEGER, "Modification" ), |
|
|
|
DEFINE_FIELD( m_bPlacedByPlayer, FIELD_BOOLEAN ), |
|
DEFINE_FIELD( m_bHeldByPhysgun, FIELD_BOOLEAN ), |
|
|
|
DEFINE_FIELD( m_iFlipAttempts, FIELD_INTEGER ), |
|
|
|
DEFINE_FIELD( m_flTimeGrabbed, FIELD_TIME ), |
|
DEFINE_FIELD( m_iMineState, FIELD_INTEGER ), |
|
|
|
// Physics Influence |
|
DEFINE_FIELD( m_hPhysicsAttacker, FIELD_EHANDLE ), |
|
DEFINE_FIELD( m_flLastPhysicsInfluenceTime, FIELD_TIME ), |
|
|
|
DEFINE_PHYSPTR( m_pConstraint ), |
|
|
|
DEFINE_OUTPUT( m_OnPulledUp, "OnPulledUp" ), |
|
DEFINE_INPUTFUNC( FIELD_VOID, "Disarm", InputDisarm ), |
|
|
|
END_DATADESC() |
|
|
|
string_t CBounceBomb::gm_iszFloorTurretClassname; |
|
string_t CBounceBomb::gm_iszGroundTurretClassname; |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::Precache() |
|
{ |
|
PrecacheModel("models/props_combine/combine_mine01.mdl"); |
|
|
|
PrecacheScriptSound( "NPC_CombineMine.Hop" ); |
|
PrecacheScriptSound( "NPC_CombineMine.FlipOver" ); |
|
PrecacheScriptSound( "NPC_CombineMine.TurnOn" ); |
|
PrecacheScriptSound( "NPC_CombineMine.TurnOff" ); |
|
PrecacheScriptSound( "NPC_CombineMine.OpenHooks" ); |
|
PrecacheScriptSound( "NPC_CombineMine.CloseHooks" ); |
|
|
|
PrecacheScriptSound( "NPC_CombineMine.ActiveLoop" ); |
|
|
|
PrecacheModel( "sprites/glow01.vmt" ); |
|
|
|
gm_iszFloorTurretClassname = AllocPooledString( "npc_turret_floor" ); |
|
gm_iszGroundTurretClassname = AllocPooledString( "npc_turret_ground" ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::Spawn() |
|
{ |
|
Precache(); |
|
|
|
Wake( false ); |
|
|
|
SetModel("models/props_combine/combine_mine01.mdl"); |
|
|
|
SetSolid( SOLID_VPHYSICS ); |
|
|
|
m_hSprite.Set( NULL ); |
|
m_takedamage = DAMAGE_EVENTS_ONLY; |
|
|
|
// Find my feet! |
|
m_iHookN = LookupPoseParameter( "blendnorth" ); |
|
m_iHookE = LookupPoseParameter( "blendeast" ); |
|
m_iHookS = LookupPoseParameter( "blendsouth" ); |
|
m_iAllHooks = LookupPoseParameter( "blendstates" ); |
|
m_flHookPositions = 0; |
|
|
|
SetHealth( 100 ); |
|
|
|
m_bBounce = true; |
|
|
|
SetSequence( SelectWeightedSequence( ACT_IDLE ) ); |
|
|
|
OpenHooks( true ); |
|
|
|
m_bHeldByPhysgun = false; |
|
|
|
m_iFlipAttempts = 0; |
|
|
|
if( !GetParent() ) |
|
{ |
|
// Create vphysics now if I'm not being carried. |
|
CreateVPhysics(); |
|
} |
|
|
|
m_flTimeGrabbed = FLT_MAX; |
|
|
|
if( m_bDisarmed ) |
|
{ |
|
SetMineState( MINE_STATE_DORMANT ); |
|
} |
|
else |
|
{ |
|
SetMineState( MINE_STATE_DEPLOY ); |
|
} |
|
|
|
// default to a different skin for cavern turrets (unless explicitly overridden) |
|
if ( m_iModification == MINE_MODIFICATION_CAVERN ) |
|
{ |
|
// look for this value in the first datamap |
|
// loop through the data description list, restoring each data desc block |
|
datamap_t *dmap = GetDataDescMap(); |
|
|
|
bool bFoundSkin = false; |
|
// search through all the readable fields in the data description, looking for a match |
|
for ( int i = 0; i < dmap->dataNumFields; ++i ) |
|
{ |
|
if ( dmap->dataDesc[i].flags & (FTYPEDESC_OUTPUT | FTYPEDESC_KEY) ) |
|
{ |
|
if ( !Q_stricmp(dmap->dataDesc[i].externalName, "Skin") ) |
|
{ |
|
bFoundSkin = true; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if (!bFoundSkin) |
|
{ |
|
// select a random skin for the mine. Actually, we'll cycle through the available skins |
|
// using a static variable to provide better distribution. The static isn't saved but |
|
// really it's only cosmetic. |
|
static unsigned int nextSkin = MINE_CITIZEN_SKIN_MIN; |
|
m_nSkin = nextSkin; |
|
// increment the skin for next time |
|
nextSkin = (nextSkin >= MINE_CITIZEN_SKIN_MAX) ? MINE_CITIZEN_SKIN_MIN : nextSkin + 1; |
|
} |
|
|
|
// pretend like the player set me down. |
|
m_bPlacedByPlayer = true; |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::OnRestore() |
|
{ |
|
BaseClass::OnRestore(); |
|
if ( gpGlobals->eLoadType == MapLoad_Transition && !m_hSprite && m_LastSpriteColor.GetRawColor() != 0 ) |
|
{ |
|
UpdateLight( true, m_LastSpriteColor.r(), m_LastSpriteColor.g(), m_LastSpriteColor.b(), m_LastSpriteColor.a() ); |
|
} |
|
|
|
if( VPhysicsGetObject() ) |
|
{ |
|
VPhysicsGetObject()->Wake(); |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
int CBounceBomb::DrawDebugTextOverlays(void) |
|
{ |
|
int text_offset = BaseClass::DrawDebugTextOverlays(); |
|
if (m_debugOverlays & OVERLAY_TEXT_BIT) |
|
{ |
|
char tempstr[512]; |
|
Q_snprintf(tempstr,sizeof(tempstr), "%s", pszMineStateNames[m_iMineState] ); |
|
EntityText(text_offset,tempstr,0); |
|
text_offset++; |
|
} |
|
return text_offset; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::SetMineState( int iState ) |
|
{ |
|
m_iMineState = iState; |
|
|
|
switch( iState ) |
|
{ |
|
case MINE_STATE_DORMANT: |
|
{ |
|
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); |
|
controller.SoundChangeVolume( m_pWarnSound, 0.0, 0.1 ); |
|
UpdateLight( false, 0, 0, 0, 0 ); |
|
SetThink( NULL ); |
|
} |
|
break; |
|
|
|
case MINE_STATE_CAPTIVE: |
|
{ |
|
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); |
|
controller.SoundChangeVolume( m_pWarnSound, 0.0, 0.2 ); |
|
|
|
// Unhook |
|
unsigned int flags = VPhysicsGetObject()->GetCallbackFlags(); |
|
VPhysicsGetObject()->SetCallbackFlags( flags | CALLBACK_GLOBAL_TOUCH_STATIC ); |
|
OpenHooks(); |
|
physenv->DestroyConstraint( m_pConstraint ); |
|
m_pConstraint = NULL; |
|
|
|
UpdateLight( true, 0, 0, 255, 190 ); |
|
SetThink( &CBounceBomb::CaptiveThink ); |
|
SetNextThink( gpGlobals->curtime + 0.1f ); |
|
SetTouch( NULL ); |
|
} |
|
break; |
|
|
|
case MINE_STATE_DEPLOY: |
|
OpenHooks( true ); |
|
UpdateLight( true, 0, 0, 255, 190 ); |
|
SetThink( &CBounceBomb::SettleThink ); |
|
SetTouch( NULL ); |
|
SetNextThink( gpGlobals->curtime + 0.1f ); |
|
break; |
|
|
|
case MINE_STATE_ARMED: |
|
UpdateLight( false, 0, 0, 0, 0 ); |
|
SetThink( &CBounceBomb::SearchThink ); |
|
SetNextThink( gpGlobals->curtime + 0.1f ); |
|
break; |
|
|
|
case MINE_STATE_TRIGGERED: |
|
{ |
|
OpenHooks(); |
|
|
|
if( m_pConstraint ) |
|
{ |
|
physenv->DestroyConstraint( m_pConstraint ); |
|
m_pConstraint = NULL; |
|
} |
|
|
|
// Scare NPC's |
|
CSoundEnt::InsertSound( SOUND_DANGER, GetAbsOrigin(), 300, 1.0f, this ); |
|
|
|
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); |
|
controller.SoundChangeVolume( m_pWarnSound, 0.0, 0.2 ); |
|
|
|
SetTouch( &CBounceBomb::ExplodeTouch ); |
|
unsigned int flags = VPhysicsGetObject()->GetCallbackFlags(); |
|
VPhysicsGetObject()->SetCallbackFlags( flags | CALLBACK_GLOBAL_TOUCH_STATIC ); |
|
|
|
Vector vecNudge; |
|
|
|
vecNudge.x = random->RandomFloat( -1, 1 ); |
|
vecNudge.y = random->RandomFloat( -1, 1 ); |
|
vecNudge.z = 1.5; |
|
vecNudge *= 350; |
|
|
|
VPhysicsGetObject()->Wake(); |
|
VPhysicsGetObject()->ApplyForceCenter( vecNudge ); |
|
|
|
float x, y; |
|
x = 10 + random->RandomFloat( 0, 20 ); |
|
y = 10 + random->RandomFloat( 0, 20 ); |
|
|
|
VPhysicsGetObject()->ApplyTorqueCenter( AngularImpulse( x, y, 0 ) ); |
|
|
|
// Since we just nudged the mine, ignore collisions with the world until |
|
// the mine is in the air. We only want to explode if the player tries to |
|
// run over the mine before it jumps up. |
|
m_flIgnoreWorldTime = gpGlobals->curtime + 1.0; |
|
UpdateLight( true, 255, 0, 0, 190 ); |
|
|
|
// use the correct bounce behavior |
|
if (m_iModification == MINE_MODIFICATION_CAVERN) |
|
{ |
|
SetThink ( &CBounceBomb::CavernBounceThink ); |
|
SetNextThink( gpGlobals->curtime + 0.15 ); |
|
} |
|
else |
|
{ |
|
SetThink( &CBounceBomb::BounceThink ); |
|
SetNextThink( gpGlobals->curtime + 0.5 ); |
|
} |
|
} |
|
break; |
|
|
|
case MINE_STATE_LAUNCHED: |
|
{ |
|
UpdateLight( true, 255, 0, 0, 190 ); |
|
SetThink( NULL ); |
|
SetNextThink( gpGlobals->curtime + 0.5 ); |
|
|
|
SetTouch( &CBounceBomb::ExplodeTouch ); |
|
unsigned int flags = VPhysicsGetObject()->GetCallbackFlags(); |
|
VPhysicsGetObject()->SetCallbackFlags( flags | CALLBACK_GLOBAL_TOUCH_STATIC ); |
|
} |
|
break; |
|
|
|
default: |
|
DevMsg("**Unknown Mine State: %d\n", iState ); |
|
break; |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
// Bouncbomb flips to try to right itself, try to get off |
|
// of and object that it's not allowed to clamp to, or |
|
// to get away from a hint node that inhibits placement |
|
// of mines. |
|
//--------------------------------------------------------- |
|
void CBounceBomb::Flip( const Vector &vecForce, const AngularImpulse &torque ) |
|
{ |
|
if( m_iFlipAttempts > BOUNCEBOMB_MAX_FLIPS ) |
|
{ |
|
// Not allowed to try anymore. |
|
SetThink(NULL); |
|
return; |
|
} |
|
|
|
EmitSound( "NPC_CombineMine.FlipOver" ); |
|
VPhysicsGetObject()->ApplyForceCenter( vecForce ); |
|
VPhysicsGetObject()->ApplyTorqueCenter( torque ); |
|
m_iFlipAttempts++; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
#define MINE_MIN_PROXIMITY_SQR 676 // 27 inches |
|
bool CBounceBomb::IsValidLocation() |
|
{ |
|
CBaseEntity *pAvoidObject = NULL; |
|
float flAvoidForce = 0.0f; |
|
CAI_Hint *pHint; |
|
CHintCriteria criteria; |
|
criteria.SetHintType( HINT_WORLD_INHIBIT_COMBINE_MINES ); |
|
criteria.SetFlag( bits_HINT_NODE_NEAREST ); |
|
criteria.AddIncludePosition( GetAbsOrigin(), 12.0f * 15.0f ); |
|
pHint = CAI_HintManager::FindHint( GetAbsOrigin(), criteria ); |
|
|
|
if( pHint ) |
|
{ |
|
pAvoidObject = pHint; |
|
flAvoidForce = 120.0f; |
|
} |
|
else |
|
{ |
|
// Look for other mines that are too close to me. |
|
CBaseEntity *pEntity = gEntList.FirstEnt(); |
|
Vector vecMyPosition = GetAbsOrigin(); |
|
while( pEntity ) |
|
{ |
|
if( pEntity->m_iClassname == m_iClassname && pEntity != this ) |
|
{ |
|
// Don't lock down if I'm near a mine that's already locked down. |
|
if( vecMyPosition.DistToSqr(pEntity->GetAbsOrigin()) < MINE_MIN_PROXIMITY_SQR ) |
|
{ |
|
pAvoidObject = pEntity; |
|
flAvoidForce = 60.0f; |
|
break; |
|
} |
|
} |
|
|
|
pEntity = gEntList.NextEnt( pEntity ); |
|
} |
|
} |
|
|
|
if( pAvoidObject ) |
|
{ |
|
// Build a force vector to push us away from the inhibitor. |
|
// Start by pushing upwards. |
|
Vector vecForce = Vector( 0, 0, VPhysicsGetObject()->GetMass() * 200.0f ); |
|
|
|
// Now add some force in the direction that takes us away from the inhibitor. |
|
Vector vecDir = GetAbsOrigin() - pAvoidObject->GetAbsOrigin(); |
|
vecDir.z = 0.0f; |
|
VectorNormalize( vecDir ); |
|
vecForce += vecDir * VPhysicsGetObject()->GetMass() * flAvoidForce; |
|
|
|
Flip( vecForce, AngularImpulse( 100, 0, 0 ) ); |
|
|
|
// Tell the code that asked that this position isn't valid. |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
// Release the spikes |
|
//--------------------------------------------------------- |
|
void CBounceBomb::BounceThink() |
|
{ |
|
SetNextThink( gpGlobals->curtime + 0.1 ); |
|
StudioFrameAdvance(); |
|
|
|
IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); |
|
|
|
if ( pPhysicsObject != NULL ) |
|
{ |
|
const float MINE_MAX_JUMP_HEIGHT = 200; |
|
|
|
// Figure out how much headroom the mine has, and hop to within a few inches of that. |
|
trace_t tr; |
|
UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, MINE_MAX_JUMP_HEIGHT ), MASK_SHOT, this, COLLISION_GROUP_INTERACTIVE, &tr ); |
|
|
|
float height; |
|
|
|
if( tr.m_pEnt && tr.m_pEnt->VPhysicsGetObject() ) |
|
{ |
|
// Physics object resting on me. Jump as hard as allowed to try to knock it away. |
|
height = MINE_MAX_JUMP_HEIGHT; |
|
} |
|
else |
|
{ |
|
height = tr.endpos.z - GetAbsOrigin().z; |
|
height -= BOUNCEBOMB_RADIUS; |
|
if ( height < 0.1 ) |
|
height = 0.1; |
|
} |
|
|
|
float time = sqrt( height / (0.5 * GetCurrentGravity()) ); |
|
float velocity = GetCurrentGravity() * time; |
|
|
|
// or you can just AddVelocity to the object instead of ApplyForce |
|
float force = velocity * pPhysicsObject->GetMass(); |
|
|
|
Vector up; |
|
|
|
GetVectors( NULL, NULL, &up ); |
|
pPhysicsObject->Wake(); |
|
pPhysicsObject->ApplyForceCenter( up * force ); |
|
|
|
pPhysicsObject->ApplyTorqueCenter( AngularImpulse( random->RandomFloat( 5, 25 ), random->RandomFloat( 5, 25 ), 0 ) ); |
|
|
|
|
|
if( m_hNearestNPC ) |
|
{ |
|
Vector vecPredict = m_hNearestNPC->GetSmoothedVelocity(); |
|
|
|
pPhysicsObject->ApplyForceCenter( vecPredict * 10 ); |
|
} |
|
|
|
EmitSound( "NPC_CombineMine.Hop" ); |
|
SetThink( NULL ); |
|
} |
|
} |
|
|
|
|
|
//--------------------------------------------------------- |
|
// A different bounce behavior for the citizen-modified mine. Detonates at the top of its apex, |
|
// and does not attempt to track enemies. |
|
//--------------------------------------------------------- |
|
void CBounceBomb::CavernBounceThink() |
|
{ |
|
SetNextThink( gpGlobals->curtime + 0.1 ); |
|
StudioFrameAdvance(); |
|
|
|
IPhysicsObject *pPhysicsObject = VPhysicsGetObject(); |
|
|
|
if ( pPhysicsObject != NULL ) |
|
{ |
|
const float MINE_MAX_JUMP_HEIGHT = 78; |
|
|
|
// Figure out how much headroom the mine has, and hop to within a few inches of that. |
|
trace_t tr; |
|
UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() + Vector( 0, 0, MINE_MAX_JUMP_HEIGHT ), MASK_SHOT, this, COLLISION_GROUP_INTERACTIVE, &tr ); |
|
|
|
float height; |
|
|
|
if( tr.m_pEnt && tr.m_pEnt->VPhysicsGetObject() ) |
|
{ |
|
// Physics object resting on me. Jump as hard as allowed to try to knock it away. |
|
height = MINE_MAX_JUMP_HEIGHT; |
|
} |
|
else |
|
{ |
|
height = tr.endpos.z - GetAbsOrigin().z; |
|
height -= BOUNCEBOMB_RADIUS; |
|
if ( height < 0.1 ) |
|
height = 0.1; |
|
} |
|
|
|
float time = sqrt( height / (0.5 * GetCurrentGravity()) ); |
|
float velocity = GetCurrentGravity() * time; |
|
|
|
// or you can just AddVelocity to the object instead of ApplyForce |
|
float force = velocity * pPhysicsObject->GetMass(); |
|
|
|
Vector up; |
|
|
|
GetVectors( NULL, NULL, &up ); |
|
|
|
pPhysicsObject->Wake(); |
|
pPhysicsObject->ApplyForceCenter( up * force ); |
|
if( m_hNearestNPC ) |
|
{ |
|
Vector vecPredict = m_hNearestNPC->GetSmoothedVelocity(); |
|
|
|
pPhysicsObject->ApplyForceCenter( vecPredict * (pPhysicsObject->GetMass() * 0.65f) ); |
|
} |
|
|
|
pPhysicsObject->ApplyTorqueCenter( AngularImpulse( random->RandomFloat( 15, 40 ), random->RandomFloat( 15, 40 ), random->RandomFloat( 30, 60 ) ) ); |
|
|
|
EmitSound( "NPC_CombineMine.Hop" ); |
|
|
|
SetThink( &CBounceBomb::ExplodeThink ); |
|
SetNextThink( gpGlobals->curtime + 0.33f ); |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::CaptiveThink() |
|
{ |
|
SetNextThink( gpGlobals->curtime + 0.05 ); |
|
StudioFrameAdvance(); |
|
|
|
float phase = fabs( sin( gpGlobals->curtime * 4.0f ) ); |
|
phase *= BOUNCEBOMB_HOOK_RANGE; |
|
SetPoseParameter( m_iAllHooks, phase ); |
|
return; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::SettleThink() |
|
{ |
|
SetNextThink( gpGlobals->curtime + 0.05 ); |
|
StudioFrameAdvance(); |
|
|
|
if( GetParent() ) |
|
{ |
|
// A scanner or something is carrying me. Just keep checking back. |
|
return; |
|
} |
|
|
|
// Not being carried. |
|
if( !VPhysicsGetObject() ) |
|
{ |
|
// Probably was just dropped. Get physics going. |
|
CreateVPhysics(); |
|
|
|
if( !VPhysicsGetObject() ) |
|
{ |
|
Msg("**** Can't create vphysics for combine_mine!\n" ); |
|
UTIL_Remove( this ); |
|
return; |
|
} |
|
|
|
VPhysicsGetObject()->Wake(); |
|
return; |
|
} |
|
|
|
if( !m_bDisarmed ) |
|
{ |
|
if( VPhysicsGetObject()->IsAsleep() && !(VPhysicsGetObject()->GetGameFlags() & FVPHYSICS_PLAYER_HELD) ) |
|
{ |
|
// If i'm not resting on the world, jump randomly. |
|
trace_t tr; |
|
UTIL_TraceLine( GetAbsOrigin(), GetAbsOrigin() - Vector( 0, 0, 1024 ), MASK_SHOT|CONTENTS_GRATE, this, COLLISION_GROUP_NONE, &tr ); |
|
|
|
bool bHop = false; |
|
if( tr.m_pEnt ) |
|
{ |
|
IPhysicsObject *pPhysics = tr.m_pEnt->VPhysicsGetObject(); |
|
|
|
if( pPhysics && pPhysics->GetMass() <= 1000 ) |
|
{ |
|
// Light physics objects can be moved out from under the mine. |
|
bHop = true; |
|
} |
|
else if( tr.m_pEnt->m_takedamage != DAMAGE_NO ) |
|
{ |
|
// Things that can be harmed can likely be broken. |
|
bHop = true; |
|
} |
|
|
|
if( bHop ) |
|
{ |
|
Vector vecForce; |
|
vecForce.x = random->RandomFloat( -1000, 1000 ); |
|
vecForce.y = random->RandomFloat( -1000, 1000 ); |
|
vecForce.z = 2500; |
|
|
|
AngularImpulse torque( 160, 0, 160 ); |
|
|
|
Flip( vecForce, torque ); |
|
return; |
|
} |
|
|
|
// Check for upside-down |
|
Vector vecUp; |
|
GetVectors( NULL, NULL, &vecUp ); |
|
if( vecUp.z <= 0.8 ) |
|
{ |
|
// Landed upside down. Right self |
|
Vector vecForce( 0, 0, 2500 ); |
|
Flip( vecForce, AngularImpulse( 60, 0, 0 ) ); |
|
return; |
|
} |
|
} |
|
|
|
// Check to make sure I'm not in a forbidden location |
|
if( !IsValidLocation() ) |
|
{ |
|
return; |
|
} |
|
|
|
// Lock to what I'm resting on |
|
constraint_ballsocketparams_t ballsocket; |
|
ballsocket.Defaults(); |
|
ballsocket.constraint.Defaults(); |
|
ballsocket.constraint.forceLimit = lbs2kg(1000); |
|
ballsocket.constraint.torqueLimit = lbs2kg(1000); |
|
ballsocket.InitWithCurrentObjectState( g_PhysWorldObject, VPhysicsGetObject(), GetAbsOrigin() ); |
|
m_pConstraint = physenv->CreateBallsocketConstraint( g_PhysWorldObject, VPhysicsGetObject(), NULL, ballsocket ); |
|
CloseHooks(); |
|
|
|
SetMineState( MINE_STATE_ARMED ); |
|
} |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
int CBounceBomb::OnTakeDamage( const CTakeDamageInfo &info ) |
|
{ |
|
if( m_pConstraint || !VPhysicsGetObject()) |
|
{ |
|
return false; |
|
} |
|
|
|
VPhysicsTakeDamage( info ); |
|
return true; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::UpdateLight( bool bTurnOn, unsigned int r, unsigned int g, unsigned int b, unsigned int a ) |
|
{ |
|
if( bTurnOn ) |
|
{ |
|
Assert( a > 0 ); |
|
|
|
// Throw the old sprite away |
|
if( m_hSprite ) |
|
{ |
|
UTIL_Remove( m_hSprite ); |
|
m_hSprite.Set( NULL ); |
|
} |
|
|
|
if( !m_hSprite.Get() ) |
|
{ |
|
Vector up; |
|
GetVectors( NULL, NULL, &up ); |
|
|
|
// Light isn't on. |
|
m_hSprite = CSprite::SpriteCreate( "sprites/glow01.vmt", GetAbsOrigin() + up * 10.0f, false ); |
|
CSprite *pSprite = (CSprite *)m_hSprite.Get(); |
|
|
|
if( m_hSprite ) |
|
{ |
|
pSprite->SetParent( this ); |
|
pSprite->SetTransparency( kRenderTransAdd, r, g, b, a, kRenderFxNone ); |
|
pSprite->SetScale( 0.35, 0.0 ); |
|
} |
|
} |
|
else |
|
{ |
|
// Update color |
|
CSprite *pSprite = (CSprite *)m_hSprite.Get(); |
|
pSprite->SetTransparency( kRenderTransAdd, r, g, b, a, kRenderFxNone ); |
|
} |
|
} |
|
|
|
if( !bTurnOn ) |
|
{ |
|
if( m_hSprite ) |
|
{ |
|
UTIL_Remove( m_hSprite ); |
|
m_hSprite.Set( NULL ); |
|
} |
|
} |
|
|
|
if ( !m_hSprite ) |
|
{ |
|
m_LastSpriteColor.SetRawColor( 0 ); |
|
} |
|
else |
|
{ |
|
m_LastSpriteColor.SetColor( r, g, b, a ); |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::Wake( bool bAwake ) |
|
{ |
|
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); |
|
|
|
CReliableBroadcastRecipientFilter filter; |
|
|
|
if( !m_pWarnSound ) |
|
{ |
|
m_pWarnSound = controller.SoundCreate( filter, entindex(), "NPC_CombineMine.ActiveLoop" ); |
|
controller.Play( m_pWarnSound, 1.0, PITCH_NORM ); |
|
} |
|
|
|
if( bAwake ) |
|
{ |
|
// Turning on |
|
if( m_bFoeNearest ) |
|
{ |
|
EmitSound( "NPC_CombineMine.TurnOn" ); |
|
controller.SoundChangeVolume( m_pWarnSound, 1.0, 0.1 ); |
|
} |
|
|
|
unsigned char r, g, b; |
|
r = g = b = 0; |
|
|
|
if( m_bFoeNearest ) |
|
{ |
|
r = 255; |
|
} |
|
else |
|
{ |
|
g = 255; |
|
} |
|
|
|
UpdateLight( true, r, g, b, 190 ); |
|
} |
|
else |
|
{ |
|
// Turning off |
|
if( m_bFoeNearest ) |
|
{ |
|
EmitSound( "NPC_CombineMine.TurnOff" ); |
|
} |
|
|
|
SetNearestNPC( NULL ); |
|
controller.SoundChangeVolume( m_pWarnSound, 0.0, 0.1 ); |
|
UpdateLight( false, 0, 0, 0, 0 ); |
|
} |
|
|
|
m_bAwake = bAwake; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
// Returns distance to the nearest BaseCombatCharacter. |
|
//--------------------------------------------------------- |
|
float CBounceBomb::FindNearestNPC() |
|
{ |
|
float flNearest = (BOUNCEBOMB_WARN_RADIUS * BOUNCEBOMB_WARN_RADIUS) + 1.0; |
|
|
|
// Assume this search won't find anyone. |
|
SetNearestNPC( NULL ); |
|
|
|
CAI_BaseNPC **ppAIs = g_AI_Manager.AccessAIs(); |
|
int nAIs = g_AI_Manager.NumAIs(); |
|
|
|
for ( int i = 0; i < nAIs; i++ ) |
|
{ |
|
CAI_BaseNPC *pNPC = ppAIs[ i ]; |
|
|
|
if( pNPC->IsAlive() ) |
|
{ |
|
// ignore hidden objects |
|
if ( pNPC->IsEffectActive( EF_NODRAW ) ) |
|
continue; |
|
|
|
// Don't bother with NPC's that are below me. |
|
if( pNPC->EyePosition().z < GetAbsOrigin().z ) |
|
continue; |
|
|
|
// Disregard things that want to be disregarded |
|
if( pNPC->Classify() == CLASS_NONE ) |
|
continue; |
|
|
|
// Disregard bullseyes |
|
if( pNPC->Classify() == CLASS_BULLSEYE ) |
|
continue; |
|
|
|
// Disregard turrets |
|
if( pNPC->m_iClassname == gm_iszFloorTurretClassname || pNPC->m_iClassname == gm_iszGroundTurretClassname ) |
|
continue; |
|
|
|
|
|
float flDist = (GetAbsOrigin() - pNPC->GetAbsOrigin()).LengthSqr(); |
|
|
|
if( flDist < flNearest ) |
|
{ |
|
// Now do a visibility test. |
|
if( FVisible( pNPC, MASK_SOLID_BRUSHONLY ) ) |
|
{ |
|
flNearest = flDist; |
|
SetNearestNPC( pNPC ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
// finally, check the player. |
|
CBasePlayer *pPlayer = UTIL_GetLocalPlayer(); |
|
|
|
if( pPlayer && !(pPlayer->GetFlags() & FL_NOTARGET) ) |
|
{ |
|
float flDist = (pPlayer->GetAbsOrigin() - GetAbsOrigin() ).LengthSqr(); |
|
|
|
if( flDist < flNearest && FVisible( pPlayer, MASK_SOLID_BRUSHONLY ) ) |
|
{ |
|
flNearest = flDist; |
|
SetNearestNPC( pPlayer ); |
|
} |
|
} |
|
|
|
if( m_hNearestNPC.Get() ) |
|
{ |
|
// If sprite is active, update its color to reflect who is nearest. |
|
if( IsFriend( m_hNearestNPC ) ) |
|
{ |
|
if( m_bFoeNearest ) |
|
{ |
|
// Changing state to where a friend is nearest. |
|
|
|
if( IsFriend( m_hNearestNPC ) ) |
|
{ |
|
// Friend |
|
UpdateLight( true, 0, 255, 0, 190 ); |
|
m_bFoeNearest = false; |
|
} |
|
} |
|
} |
|
else // it's a foe |
|
{ |
|
if( !m_bFoeNearest ) |
|
{ |
|
// Changing state to where a foe is nearest. |
|
UpdateLight( true, 255, 0, 0, 190 ); |
|
m_bFoeNearest = true; |
|
} |
|
} |
|
} |
|
|
|
return sqrt( flNearest ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
bool CBounceBomb::IsFriend( CBaseEntity *pEntity ) |
|
{ |
|
int classify = pEntity->Classify(); |
|
bool bIsCombine = false; |
|
|
|
// Unconditional enemies to combine and Player. |
|
if( classify == CLASS_ZOMBIE || classify == CLASS_HEADCRAB || classify == CLASS_ANTLION ) |
|
{ |
|
return false; |
|
} |
|
|
|
if( classify == CLASS_METROPOLICE || |
|
classify == CLASS_COMBINE || |
|
classify == CLASS_MILITARY || |
|
classify == CLASS_COMBINE_HUNTER || |
|
classify == CLASS_SCANNER ) |
|
{ |
|
bIsCombine = true; |
|
} |
|
|
|
if( m_bPlacedByPlayer ) |
|
{ |
|
return !bIsCombine; |
|
} |
|
else |
|
{ |
|
return bIsCombine; |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::SearchThink() |
|
{ |
|
if( !UTIL_FindClientInPVS(edict()) ) |
|
{ |
|
// Sleep! |
|
SetNextThink( gpGlobals->curtime + 0.5 ); |
|
return; |
|
} |
|
|
|
if( (CAI_BaseNPC::m_nDebugBits & bits_debugDisableAI) ) |
|
{ |
|
if( IsAwake() ) |
|
{ |
|
Wake(false); |
|
} |
|
|
|
SetNextThink( gpGlobals->curtime + 0.5 ); |
|
return; |
|
} |
|
|
|
SetNextThink( gpGlobals->curtime + 0.1 ); |
|
StudioFrameAdvance(); |
|
|
|
if( m_pConstraint && gpGlobals->curtime - m_flTimeGrabbed >= 1.0f ) |
|
{ |
|
m_OnPulledUp.FireOutput( this, this ); |
|
SetMineState( MINE_STATE_CAPTIVE ); |
|
return; |
|
} |
|
|
|
float flNearestNPCDist = FindNearestNPC(); |
|
|
|
if( flNearestNPCDist <= BOUNCEBOMB_WARN_RADIUS ) |
|
{ |
|
if( !IsAwake() ) |
|
{ |
|
Wake( true ); |
|
} |
|
} |
|
else |
|
{ |
|
if( IsAwake() ) |
|
{ |
|
Wake( false ); |
|
} |
|
|
|
return; |
|
} |
|
|
|
if( flNearestNPCDist <= BOUNCEBOMB_DETONATE_RADIUS && !IsFriend( m_hNearestNPC ) ) |
|
{ |
|
if( m_bBounce ) |
|
{ |
|
SetMineState( MINE_STATE_TRIGGERED ); |
|
} |
|
else |
|
{ |
|
// Don't pop up in the air, just explode if the NPC gets closer than explode radius. |
|
SetThink( &CBounceBomb::ExplodeThink ); |
|
SetNextThink( gpGlobals->curtime + m_flExplosionDelay ); |
|
} |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::ExplodeTouch( CBaseEntity *pOther ) |
|
{ |
|
// Don't touch anything if held by physgun. |
|
if( m_bHeldByPhysgun ) |
|
return; |
|
|
|
// Don't touch triggers. |
|
if( pOther->IsSolidFlagSet(FSOLID_TRIGGER) ) |
|
return; |
|
|
|
// Don't touch gibs and other debris |
|
if( pOther->GetCollisionGroup() == COLLISION_GROUP_DEBRIS ) |
|
{ |
|
if( hl2_episodic.GetBool() ) |
|
{ |
|
Vector vecVelocity; |
|
|
|
VPhysicsGetObject()->GetVelocity( &vecVelocity, NULL ); |
|
|
|
if( vecVelocity == vec3_origin ) |
|
{ |
|
ExplodeThink(); |
|
} |
|
} |
|
|
|
return; |
|
} |
|
|
|
// Don't detonate against the world if not allowed. Actually, don't |
|
// detonate against anything that's probably not an NPC (such as physics props) |
|
if( m_flIgnoreWorldTime > gpGlobals->curtime && !pOther->MyCombatCharacterPointer() ) |
|
{ |
|
return; |
|
} |
|
|
|
ExplodeThink(); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::ExplodeThink() |
|
{ |
|
SetSolid( SOLID_NONE ); |
|
|
|
// Don't catch self in own explosion! |
|
m_takedamage = DAMAGE_NO; |
|
|
|
if( m_hSprite ) |
|
{ |
|
UpdateLight( false, 0, 0, 0, 0 ); |
|
} |
|
|
|
if( m_pWarnSound ) |
|
{ |
|
CSoundEnvelopeController &controller = CSoundEnvelopeController::GetController(); |
|
controller.SoundDestroy( m_pWarnSound ); |
|
} |
|
|
|
|
|
CBaseEntity *pThrower = HasPhysicsAttacker( 0.5 ); |
|
|
|
if (m_iModification == MINE_MODIFICATION_CAVERN) |
|
{ |
|
ExplosionCreate( GetAbsOrigin(), GetAbsAngles(), (pThrower) ? pThrower : this, BOUNCEBOMB_EXPLODE_DAMAGE, BOUNCEBOMB_EXPLODE_RADIUS, true, |
|
NULL, CLASS_PLAYER_ALLY ); |
|
} |
|
else |
|
{ |
|
ExplosionCreate( GetAbsOrigin(), GetAbsAngles(), (pThrower) ? pThrower : this, BOUNCEBOMB_EXPLODE_DAMAGE, BOUNCEBOMB_EXPLODE_RADIUS, true); |
|
} |
|
UTIL_Remove( this ); |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::OpenHooks( bool bSilent ) |
|
{ |
|
if( !bSilent ) |
|
{ |
|
EmitSound( "NPC_CombineMine.OpenHooks" ); |
|
} |
|
|
|
if( VPhysicsGetObject() ) |
|
{ |
|
// It's possible to not have a valid physics object here, since this function doubles as an initialization function. |
|
PhysClearGameFlags( VPhysicsGetObject(), FVPHYSICS_CONSTRAINT_STATIC ); |
|
|
|
VPhysicsGetObject()->EnableMotion( true ); |
|
} |
|
|
|
SetPoseParameter( m_iAllHooks, BOUNCEBOMB_HOOK_RANGE ); |
|
|
|
#ifdef _XBOX |
|
RemoveEffects( EF_NOSHADOW ); |
|
#endif |
|
|
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::CloseHooks() |
|
{ |
|
if( !m_bLockSilently ) |
|
{ |
|
EmitSound( "NPC_CombineMine.CloseHooks" ); |
|
} |
|
|
|
if( VPhysicsGetObject() ) |
|
{ |
|
// It's possible to not have a valid physics object here, since this function doubles as an initialization function. |
|
PhysSetGameFlags( VPhysicsGetObject(), FVPHYSICS_CONSTRAINT_STATIC ); |
|
} |
|
|
|
// Only lock silently the first time we call this. |
|
m_bLockSilently = false; |
|
|
|
SetPoseParameter( m_iAllHooks, 0 ); |
|
|
|
VPhysicsGetObject()->EnableMotion( false ); |
|
|
|
// Once I lock down, forget how many tries it took. |
|
m_iFlipAttempts = 0; |
|
|
|
#ifdef _XBOX |
|
AddEffects( EF_NOSHADOW ); |
|
#endif |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::InputDisarm( inputdata_t &inputdata ) |
|
{ |
|
// Only affect a mine that's armed and not placed by player. |
|
if( !m_bPlacedByPlayer && m_iMineState == MINE_STATE_ARMED ) |
|
{ |
|
if( m_pConstraint ) |
|
{ |
|
physenv->DestroyConstraint( m_pConstraint ); |
|
m_pConstraint = NULL; |
|
} |
|
|
|
m_bDisarmed = true; |
|
OpenHooks(false); |
|
|
|
SetMineState(MINE_STATE_DORMANT); |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::OnPhysGunDrop( CBasePlayer *pPhysGunUser, PhysGunDrop_t Reason ) |
|
{ |
|
m_hPhysicsAttacker = pPhysGunUser; |
|
m_flLastPhysicsInfluenceTime = gpGlobals->curtime; |
|
|
|
m_flTimeGrabbed = FLT_MAX; |
|
|
|
m_bHeldByPhysgun = false; |
|
|
|
if( m_iMineState == MINE_STATE_ARMED ) |
|
{ |
|
// Put the mine back to searching. |
|
Wake( false ); |
|
return; |
|
} |
|
|
|
if( Reason == DROPPED_BY_CANNON ) |
|
{ |
|
// Set to lock down to ground again. |
|
m_bPlacedByPlayer = true; |
|
OpenHooks( true ); |
|
SetMineState( MINE_STATE_DEPLOY ); |
|
} |
|
else if ( Reason == LAUNCHED_BY_CANNON ) |
|
{ |
|
SetMineState( MINE_STATE_LAUNCHED ); |
|
} |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
CBasePlayer *CBounceBomb::HasPhysicsAttacker( float dt ) |
|
{ |
|
if (gpGlobals->curtime - dt <= m_flLastPhysicsInfluenceTime) |
|
{ |
|
return m_hPhysicsAttacker; |
|
} |
|
return NULL; |
|
} |
|
|
|
//--------------------------------------------------------- |
|
//--------------------------------------------------------- |
|
void CBounceBomb::OnPhysGunPickup( CBasePlayer *pPhysGunUser, PhysGunPickup_t reason ) |
|
{ |
|
m_hPhysicsAttacker = pPhysGunUser; |
|
m_flLastPhysicsInfluenceTime = gpGlobals->curtime; |
|
|
|
m_iFlipAttempts = 0; |
|
|
|
if( reason != PUNTED_BY_CANNON ) |
|
{ |
|
if( m_iMineState == MINE_STATE_ARMED ) |
|
{ |
|
// Yanking on a mine that is locked down, trying to rip it loose. |
|
UpdateLight( true, 255, 255, 0, 190 ); |
|
m_flTimeGrabbed = gpGlobals->curtime; |
|
m_bHeldByPhysgun = true; |
|
|
|
VPhysicsGetObject()->EnableMotion( true ); |
|
|
|
// Try to scatter NPCs without panicking them. Make a move away sound up around their |
|
// ear level. |
|
CSoundEnt::InsertSound( SOUND_MOVE_AWAY, GetAbsOrigin() + Vector( 0, 0, 60), 32.0f, 0.2f ); |
|
return; |
|
} |
|
else |
|
{ |
|
// Picked up a mine that was not locked down. |
|
m_bHeldByPhysgun = true; |
|
|
|
if( m_iMineState == MINE_STATE_TRIGGERED ) |
|
{ |
|
// This mine's already set to blow. Player can't place it. |
|
return; |
|
} |
|
else |
|
{ |
|
m_bDisarmed = false; |
|
SetMineState( MINE_STATE_DEPLOY ); |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
m_bHeldByPhysgun = false; |
|
} |
|
|
|
if( reason == PUNTED_BY_CANNON ) |
|
{ |
|
if( m_iMineState == MINE_STATE_TRIGGERED || m_iMineState == MINE_STATE_ARMED ) |
|
{ |
|
// Already set to blow |
|
return; |
|
} |
|
|
|
m_bDisarmed = false; |
|
m_bPlacedByPlayer = true; |
|
SetTouch( NULL ); |
|
SetThink( &CBounceBomb::SettleThink ); |
|
SetNextThink( gpGlobals->curtime + 0.1); |
|
|
|
// Since being punted causes the mine to flip, sometimes it 'catches an edge' |
|
// and ends up touching the ground from whence it came, exploding instantly. |
|
// This little stunt prevents that by ignoring world collisions for a very short time. |
|
m_flIgnoreWorldTime = gpGlobals->curtime + 0.1; |
|
} |
|
} |
|
|
|
|
|
LINK_ENTITY_TO_CLASS( bounce_bomb, CBounceBomb ); |
|
LINK_ENTITY_TO_CLASS( combine_bouncemine, CBounceBomb ); |
|
LINK_ENTITY_TO_CLASS( combine_mine, CBounceBomb ); |
|
|
|
/* |
|
*/
|
|
|