//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //=============================================================================// #include "cbase.h" #include "beam_shared.h" #include "player.h" #include "gamerules.h" #include "basecombatweapon.h" #include "baseviewmodel.h" #include "vphysics/constraints.h" #include "physics.h" #include "in_buttons.h" #include "IEffects.h" #include "engine/IEngineSound.h" #include "ndebugoverlay.h" #include "physics_saverestore.h" #include "player_pickup.h" #include "soundemittersystem/isoundemittersystembase.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" ConVar phys_gunmass("phys_gunmass", "200"); ConVar phys_gunvel("phys_gunvel", "400"); ConVar phys_gunforce("phys_gunforce", "5e5" ); ConVar phys_guntorque("phys_guntorque", "100" ); ConVar phys_gunglueradius("phys_gunglueradius", "128" ); static int g_physgunBeam; #define PHYSGUN_BEAM_SPRITE "sprites/physbeam.vmt" #define MAX_PELLETS 16 class CWeaponGravityGun; class CGravityPellet : public CBaseAnimating { DECLARE_CLASS( CGravityPellet, CBaseAnimating ); public: DECLARE_DATADESC(); ~CGravityPellet(); void Precache() { SetModelName( MAKE_STRING( "models/weapons/glueblob.mdl" ) ); PrecacheModel( STRING( GetModelName() ) ); BaseClass::Precache(); } void Spawn() { Precache(); SetModel( STRING( GetModelName() ) ); SetSolid( SOLID_NONE ); SetMoveType( MOVETYPE_NONE ); AddEffects( EF_NOSHADOW ); SetRenderColor( 255, 0, 0 ); m_isInert = false; } bool IsInert() { return m_isInert; } bool MakeConstraint( CBaseEntity *pObject ) { IPhysicsObject *pReference = g_PhysWorldObject; if ( GetMoveParent() ) { pReference = GetMoveParent()->VPhysicsGetObject(); } IPhysicsObject *pAttached = pObject->VPhysicsGetObject(); if ( !pReference || !pAttached ) { return false; } constraint_fixedparams_t fixed; fixed.Defaults(); fixed.InitWithCurrentObjectState( pReference, pAttached ); m_pConstraint = physenv->CreateFixedConstraint( pReference, pAttached, NULL, fixed ); m_pConstraint->SetGameData( (void *)this ); MakeInert(); return true; } void MakeInert() { SetRenderColor( 64, 64, 128 ); m_isInert = true; } void InputOnBreak( inputdata_t &inputdata ) { UTIL_Remove(this); } IPhysicsConstraint *m_pConstraint; bool m_isInert; }; LINK_ENTITY_TO_CLASS(gravity_pellet, CGravityPellet); PRECACHE_REGISTER(gravity_pellet); BEGIN_DATADESC( CGravityPellet ) DEFINE_PHYSPTR( m_pConstraint ), DEFINE_FIELD( m_isInert, FIELD_BOOLEAN ), // physics system will fire this input if the constraint breaks due to physics DEFINE_INPUTFUNC( FIELD_VOID, "ConstraintBroken", InputOnBreak ), END_DATADESC() CGravityPellet::~CGravityPellet() { if ( m_pConstraint ) { physenv->DestroyConstraint( m_pConstraint ); } } class CGravControllerPoint : public IMotionEvent { DECLARE_SIMPLE_DATADESC(); public: CGravControllerPoint( void ); ~CGravControllerPoint( void ); void AttachEntity( CBaseEntity *pEntity, IPhysicsObject *pPhys, const Vector &position ); void DetachEntity( void ); void SetMaxVelocity( float maxVel ) { m_maxVel = maxVel; } void SetTargetPosition( const Vector &target ) { m_targetPosition = target; if ( m_attachedEntity == NULL ) { m_worldPosition = target; } m_timeToArrive = gpGlobals->frametime; } void SetAutoAlign( const Vector &localDir, const Vector &localPos, const Vector &worldAlignDir, const Vector &worldAlignPos ) { m_align = true; m_localAlignNormal = -localDir; m_localAlignPosition = localPos; m_targetAlignNormal = worldAlignDir; m_targetAlignPosition = worldAlignPos; } void ClearAutoAlign() { m_align = false; } IMotionEvent::simresult_e Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ); Vector m_localPosition; Vector m_targetPosition; Vector m_worldPosition; Vector m_localAlignNormal; Vector m_localAlignPosition; Vector m_targetAlignNormal; Vector m_targetAlignPosition; bool m_align; float m_saveDamping; float m_maxVel; float m_maxAcceleration; Vector m_maxAngularAcceleration; EHANDLE m_attachedEntity; QAngle m_targetRotation; float m_timeToArrive; IPhysicsMotionController *m_controller; }; BEGIN_SIMPLE_DATADESC( CGravControllerPoint ) DEFINE_FIELD( m_localPosition, FIELD_VECTOR ), DEFINE_FIELD( m_targetPosition, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_worldPosition, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_localAlignNormal, FIELD_VECTOR ), DEFINE_FIELD( m_localAlignPosition, FIELD_VECTOR ), DEFINE_FIELD( m_targetAlignNormal, FIELD_VECTOR ), DEFINE_FIELD( m_targetAlignPosition, FIELD_POSITION_VECTOR ), DEFINE_FIELD( m_align, FIELD_BOOLEAN ), DEFINE_FIELD( m_saveDamping, FIELD_FLOAT ), DEFINE_FIELD( m_maxVel, FIELD_FLOAT ), DEFINE_FIELD( m_maxAcceleration, FIELD_FLOAT ), DEFINE_FIELD( m_maxAngularAcceleration, FIELD_VECTOR ), DEFINE_FIELD( m_attachedEntity, FIELD_EHANDLE ), DEFINE_FIELD( m_targetRotation, FIELD_VECTOR ), DEFINE_FIELD( m_timeToArrive, FIELD_FLOAT ), // Physptrs can't be saved in embedded classes... this is to silence classcheck // DEFINE_PHYSPTR( m_controller ), END_DATADESC() CGravControllerPoint::CGravControllerPoint( void ) { m_attachedEntity = NULL; } CGravControllerPoint::~CGravControllerPoint( void ) { DetachEntity(); } void CGravControllerPoint::AttachEntity( CBaseEntity *pEntity, IPhysicsObject *pPhys, const Vector &position ) { m_attachedEntity = pEntity; pPhys->WorldToLocal( &m_localPosition, position ); m_worldPosition = position; pPhys->GetDamping( NULL, &m_saveDamping ); float damping = 2; pPhys->SetDamping( NULL, &damping ); m_controller = physenv->CreateMotionController( this ); m_controller->AttachObject( pPhys, true ); m_controller->SetPriority( IPhysicsMotionController::HIGH_PRIORITY ); SetTargetPosition( position ); m_maxAcceleration = phys_gunforce.GetFloat() * pPhys->GetInvMass(); m_targetRotation = pEntity->GetAbsAngles(); float torque = phys_guntorque.GetFloat(); m_maxAngularAcceleration = torque * pPhys->GetInvInertia(); } void CGravControllerPoint::DetachEntity( void ) { CBaseEntity *pEntity = m_attachedEntity; if ( pEntity ) { IPhysicsObject *pPhys = pEntity->VPhysicsGetObject(); if ( pPhys ) { // on the odd chance that it's gone to sleep while under anti-gravity pPhys->Wake(); pPhys->SetDamping( NULL, &m_saveDamping ); } } m_attachedEntity = NULL; physenv->DestroyMotionController( m_controller ); m_controller = NULL; // UNDONE: Does this help the networking? m_targetPosition = vec3_origin; m_worldPosition = vec3_origin; } void AxisAngleQAngle( const Vector &axis, float angle, QAngle &outAngles ) { // map back to HL rotation axes outAngles.z = axis.x * angle; outAngles.x = axis.y * angle; outAngles.y = axis.z * angle; } IMotionEvent::simresult_e CGravControllerPoint::Simulate( IPhysicsMotionController *pController, IPhysicsObject *pObject, float deltaTime, Vector &linear, AngularImpulse &angular ) { Vector vel; AngularImpulse angVel; float fracRemainingSimTime = 1.0; if ( m_timeToArrive > 0 ) { fracRemainingSimTime *= deltaTime / m_timeToArrive; if ( fracRemainingSimTime > 1 ) { fracRemainingSimTime = 1; } } m_timeToArrive -= deltaTime; if ( m_timeToArrive < 0 ) { m_timeToArrive = 0; } float invDeltaTime = (1.0f / deltaTime); Vector world; pObject->LocalToWorld( &world, m_localPosition ); m_worldPosition = world; pObject->GetVelocity( &vel, &angVel ); //pObject->GetVelocityAtPoint( world, &vel ); float damping = 1.0; world += vel * deltaTime * damping; Vector delta = (m_targetPosition - world) * fracRemainingSimTime * invDeltaTime; Vector alignDir; linear = vec3_origin; angular = vec3_origin; if ( m_align ) { QAngle angles; Vector origin; Vector axis; AngularImpulse torque; pObject->GetShadowPosition( &origin, &angles ); // align local normal to target normal VMatrix tmp = SetupMatrixOrgAngles( origin, angles ); Vector worldNormal = tmp.VMul3x3( m_localAlignNormal ); axis = CrossProduct( worldNormal, m_targetAlignNormal ); float trig = VectorNormalize(axis); float alignRotation = RAD2DEG(asin(trig)); axis *= alignRotation; if ( alignRotation < 10 ) { float dot = DotProduct( worldNormal, m_targetAlignNormal ); // probably 180 degrees off if ( dot < 0 ) { if ( worldNormal.x < 0.5 ) { axis.Init(10,0,0); } else { axis.Init(0,0,10); } alignRotation = 10; } } // Solve for the rotation around the target normal (at the local align pos) that will // move the grabbed spot to the destination. Vector worldRotCenter = tmp.VMul4x3( m_localAlignPosition ); Vector rotSrc = world - worldRotCenter; Vector rotDest = m_targetPosition - worldRotCenter; // Get a basis in the plane perpendicular to m_targetAlignNormal Vector srcN = rotSrc; VectorNormalize( srcN ); Vector tangent = CrossProduct( srcN, m_targetAlignNormal ); float len = VectorNormalize( tangent ); // needs at least ~5 degrees, or forget rotation (0.08 ~= sin(5)) if ( len > 0.08 ) { Vector binormal = CrossProduct( m_targetAlignNormal, tangent ); // Now project the src & dest positions into that plane Vector planeSrc( DotProduct( rotSrc, tangent ), DotProduct( rotSrc, binormal ), 0 ); Vector planeDest( DotProduct( rotDest, tangent ), DotProduct( rotDest, binormal ), 0 ); float rotRadius = VectorNormalize( planeSrc ); float destRadius = VectorNormalize( planeDest ); if ( rotRadius > 0.1 ) { if ( destRadius < rotRadius ) { destRadius = rotRadius; } //float ratio = rotRadius / destRadius; float angleSrc = atan2( planeSrc.y, planeSrc.x ); float angleDest = atan2( planeDest.y, planeDest.x ); float angleDiff = angleDest - angleSrc; angleDiff = RAD2DEG(angleDiff); axis += m_targetAlignNormal * angleDiff; //world = m_targetPosition;// + rotDest * (1-ratio); // NDebugOverlay::Line( worldRotCenter, worldRotCenter-m_targetAlignNormal*50, 255, 0, 0, false, 0.1 ); // NDebugOverlay::Line( worldRotCenter, worldRotCenter+tangent*50, 0, 255, 0, false, 0.1 ); // NDebugOverlay::Line( worldRotCenter, worldRotCenter+binormal*50, 0, 0, 255, false, 0.1 ); } } torque = WorldToLocalRotation( tmp, axis, 1 ); torque *= fracRemainingSimTime * invDeltaTime; torque -= angVel * 1.0; // damping for ( int i = 0; i < 3; i++ ) { if ( torque[i] > 0 ) { if ( torque[i] > m_maxAngularAcceleration[i] ) torque[i] = m_maxAngularAcceleration[i]; } else { if ( torque[i] < -m_maxAngularAcceleration[i] ) torque[i] = -m_maxAngularAcceleration[i]; } } torque *= invDeltaTime; angular += torque; // Calculate an acceleration that pulls the object toward the constraint // When you're out of alignment, don't pull very hard float factor = fabsf(alignRotation); if ( factor < 5 ) { factor = clamp( factor, 0, 5 ) * (1/5); alignDir = m_targetAlignPosition - worldRotCenter; // Limit movement to the part along m_targetAlignNormal if worldRotCenter is on the backside of // of the target plane (one inch epsilon)! float planeForward = DotProduct( alignDir, m_targetAlignNormal ); if ( planeForward > 1 ) { alignDir = m_targetAlignNormal * planeForward; } Vector accel = alignDir * invDeltaTime * fracRemainingSimTime * (1-factor) * 0.20 * invDeltaTime; float mag = accel.Length(); if ( mag > m_maxAcceleration ) { accel *= (m_maxAcceleration/mag); } linear += accel; } linear -= vel*damping*invDeltaTime; // UNDONE: Factor in the change in worldRotCenter due to applied torque! } else { // clamp future velocity to max speed Vector nextVel = delta + vel; float nextSpeed = nextVel.Length(); if ( nextSpeed > m_maxVel ) { nextVel *= (m_maxVel / nextSpeed); delta = nextVel - vel; } delta *= invDeltaTime; float linearAccel = delta.Length(); if ( linearAccel > m_maxAcceleration ) { delta *= m_maxAcceleration / linearAccel; } Vector accel; AngularImpulse angAccel; pObject->CalculateForceOffset( delta, world, &accel, &angAccel ); linear += accel; angular += angAccel; } return SIM_GLOBAL_ACCELERATION; } struct pelletlist_t { DECLARE_SIMPLE_DATADESC(); Vector localNormal; // normal in parent space CHandle pellet; EHANDLE parent; }; class CWeaponGravityGun : public CBaseCombatWeapon { DECLARE_DATADESC(); public: DECLARE_CLASS( CWeaponGravityGun, CBaseCombatWeapon ); CWeaponGravityGun(); void Spawn( void ); void OnRestore( void ); void Precache( void ); void PrimaryAttack( void ); void SecondaryAttack( void ); void WeaponIdle( void ); void ItemPostFrame( void ); virtual bool Holster( CBaseCombatWeapon *pSwitchingTo ) { EffectDestroy(); return BaseClass::Holster(); } bool Reload( void ); void Equip( CBaseCombatCharacter *pOwner ) { // add constraint ammo pOwner->SetAmmoCount( MAX_PELLETS, m_iSecondaryAmmoType ); BaseClass::Equip( pOwner ); } void Drop(const Vector &vecVelocity) { CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); pOwner->SetAmmoCount( 0, m_iSecondaryAmmoType ); // destroy all constraints BaseClass::Drop(vecVelocity); } bool HasAnyAmmo( void ); void AttachObject( CBaseEntity *pEdict, const Vector& start, const Vector &end, float distance ); void DetachObject( void ); void EffectCreate( void ); void EffectUpdate( void ); void EffectDestroy( void ); void SoundCreate( void ); void SoundDestroy( void ); void SoundStop( void ); void SoundStart( void ); void SoundUpdate( void ); void AddPellet( CGravityPellet *pPellet, CBaseEntity *pParent, const Vector &surfaceNormal ); void DeleteActivePellets(); void SortPelletsForObject( CBaseEntity *pObject ); void SetObjectPelletsColor( int r, int g, int b ); void CreatePelletAttraction( float radius, CBaseEntity *pObject ); IPhysicsObject *GetPelletPhysObject( int pelletIndex ); void GetPelletWorldCoords( int pelletIndex, Vector *worldPos, Vector *worldNormal ) { if ( worldPos ) { *worldPos = m_activePellets[pelletIndex].pellet->GetAbsOrigin(); } if ( worldNormal ) { if ( m_activePellets[pelletIndex].parent ) { EntityMatrix tmp; tmp.InitFromEntity( m_activePellets[pelletIndex].parent ); *worldNormal = tmp.LocalToWorldRotation( m_activePellets[pelletIndex].localNormal ); } else { *worldNormal = m_activePellets[pelletIndex].localNormal; } } } int ObjectCaps( void ) { int caps = BaseClass::ObjectCaps(); if ( m_active ) { caps |= FCAP_DIRECTIONAL_USE; } return caps; } CBaseEntity *GetBeamEntity(); DECLARE_SERVERCLASS(); private: CNetworkVar( int, m_active ); bool m_useDown; EHANDLE m_hObject; float m_distance; float m_movementLength; float m_lastYaw; int m_soundState; CNetworkVar( int, m_viewModelIndex ); Vector m_originalObjectPosition; CGravControllerPoint m_gravCallback; pelletlist_t m_activePellets[MAX_PELLETS]; int m_pelletCount; int m_objectPelletCount; int m_pelletHeld; int m_pelletAttract; float m_glueTime; CNetworkVar( bool, m_glueTouching ); }; IMPLEMENT_SERVERCLASS_ST( CWeaponGravityGun, DT_WeaponGravityGun ) SendPropVector( SENDINFO_NAME(m_gravCallback.m_targetPosition, m_targetPosition), -1, SPROP_COORD ), SendPropVector( SENDINFO_NAME(m_gravCallback.m_worldPosition, m_worldPosition), -1, SPROP_COORD ), SendPropInt( SENDINFO(m_active), 1, SPROP_UNSIGNED ), SendPropInt( SENDINFO(m_glueTouching), 1, SPROP_UNSIGNED ), SendPropModelIndex( SENDINFO(m_viewModelIndex) ), END_SEND_TABLE() LINK_ENTITY_TO_CLASS( weapon_physgun, CWeaponGravityGun ); PRECACHE_WEAPON_REGISTER(weapon_physgun); //--------------------------------------------------------- // Save/Restore //--------------------------------------------------------- BEGIN_SIMPLE_DATADESC( pelletlist_t ) DEFINE_FIELD( localNormal, FIELD_VECTOR ), DEFINE_FIELD( pellet, FIELD_EHANDLE ), DEFINE_FIELD( parent, FIELD_EHANDLE ), END_DATADESC() BEGIN_DATADESC( CWeaponGravityGun ) DEFINE_FIELD( m_active, FIELD_INTEGER ), DEFINE_FIELD( m_useDown, FIELD_BOOLEAN ), DEFINE_FIELD( m_hObject, FIELD_EHANDLE ), DEFINE_FIELD( m_distance, FIELD_FLOAT ), DEFINE_FIELD( m_movementLength, FIELD_FLOAT ), DEFINE_FIELD( m_lastYaw, FIELD_FLOAT ), DEFINE_FIELD( m_soundState, FIELD_INTEGER ), DEFINE_FIELD( m_viewModelIndex, FIELD_INTEGER ), DEFINE_FIELD( m_originalObjectPosition, FIELD_POSITION_VECTOR ), DEFINE_EMBEDDED( m_gravCallback ), // Physptrs can't be saved in embedded classes.. DEFINE_PHYSPTR( m_gravCallback.m_controller ), DEFINE_EMBEDDED_AUTO_ARRAY( m_activePellets ), DEFINE_FIELD( m_pelletCount, FIELD_INTEGER ), DEFINE_FIELD( m_objectPelletCount, FIELD_INTEGER ), DEFINE_FIELD( m_pelletHeld, FIELD_INTEGER ), DEFINE_FIELD( m_pelletAttract, FIELD_INTEGER ), DEFINE_FIELD( m_glueTime, FIELD_TIME ), DEFINE_FIELD( m_glueTouching, FIELD_BOOLEAN ), END_DATADESC() enum physgun_soundstate { SS_SCANNING, SS_LOCKEDON }; enum physgun_soundIndex { SI_LOCKEDON = 0, SI_SCANNING = 1, SI_LIGHTOBJECT = 2, SI_HEAVYOBJECT = 3, SI_ON, SI_OFF }; //========================================================= //========================================================= CWeaponGravityGun::CWeaponGravityGun() { m_active = false; m_bFiresUnderwater = true; m_pelletAttract = -1; m_pelletHeld = -1; } //========================================================= //========================================================= void CWeaponGravityGun::Spawn( ) { BaseClass::Spawn(); // SetModel( GetWorldModel() ); FallInit(); } void CWeaponGravityGun::OnRestore( void ) { BaseClass::OnRestore(); if ( m_gravCallback.m_controller ) { m_gravCallback.m_controller->SetEventHandler( &m_gravCallback ); } } //========================================================= //========================================================= void CWeaponGravityGun::Precache( void ) { BaseClass::Precache(); g_physgunBeam = PrecacheModel(PHYSGUN_BEAM_SPRITE); PrecacheScriptSound( "Weapon_Physgun.Scanning" ); PrecacheScriptSound( "Weapon_Physgun.LockedOn" ); PrecacheScriptSound( "Weapon_Physgun.Scanning" ); PrecacheScriptSound( "Weapon_Physgun.LightObject" ); PrecacheScriptSound( "Weapon_Physgun.HeavyObject" ); } void CWeaponGravityGun::EffectCreate( void ) { EffectUpdate(); m_active = true; } void CWeaponGravityGun::EffectUpdate( void ) { Vector start, angles, forward, right; trace_t tr; CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); if ( !pOwner ) return; m_viewModelIndex = pOwner->entindex(); // Make sure I've got a view model CBaseViewModel *vm = pOwner->GetViewModel(); if ( vm ) { m_viewModelIndex = vm->entindex(); } pOwner->EyeVectors( &forward, &right, NULL ); start = pOwner->Weapon_ShootPosition(); Vector end = start + forward * 4096; UTIL_TraceLine( start, end, MASK_SHOT, pOwner, COLLISION_GROUP_NONE, &tr ); end = tr.endpos; float distance = tr.fraction * 4096; if ( tr.fraction != 1 ) { // too close to the player, drop the object if ( distance < 36 ) { DetachObject(); return; } } if ( m_hObject == NULL && tr.DidHitNonWorldEntity() ) { CBaseEntity *pEntity = tr.m_pEnt; // inform the object what was hit ClearMultiDamage(); pEntity->DispatchTraceAttack( CTakeDamageInfo( pOwner, pOwner, 0, DMG_PHYSGUN ), forward, &tr ); ApplyMultiDamage(); AttachObject( pEntity, start, tr.endpos, distance ); m_lastYaw = pOwner->EyeAngles().y; } // Add the incremental player yaw to the target transform matrix3x4_t curMatrix, incMatrix, nextMatrix; AngleMatrix( m_gravCallback.m_targetRotation, curMatrix ); AngleMatrix( QAngle(0,pOwner->EyeAngles().y - m_lastYaw,0), incMatrix ); ConcatTransforms( incMatrix, curMatrix, nextMatrix ); MatrixAngles( nextMatrix, m_gravCallback.m_targetRotation ); m_lastYaw = pOwner->EyeAngles().y; CBaseEntity *pObject = m_hObject; if ( pObject ) { if ( m_useDown ) { if ( pOwner->m_afButtonPressed & IN_USE ) { m_useDown = false; } } else { if ( pOwner->m_afButtonPressed & IN_USE ) { m_useDown = true; } } if ( m_useDown ) { pOwner->SetPhysicsFlag( PFLAG_DIROVERRIDE, true ); if ( pOwner->m_nButtons & IN_FORWARD ) { m_distance = UTIL_Approach( 1024, m_distance, gpGlobals->frametime * 100 ); } if ( pOwner->m_nButtons & IN_BACK ) { m_distance = UTIL_Approach( 40, m_distance, gpGlobals->frametime * 100 ); } } if ( pOwner->m_nButtons & IN_WEAPON1 ) { m_distance = UTIL_Approach( 1024, m_distance, m_distance * 0.1 ); } if ( pOwner->m_nButtons & IN_WEAPON2 ) { m_distance = UTIL_Approach( 40, m_distance, m_distance * 0.1 ); } // Send the object a physics damage message (0 damage). Some objects interpret this // as something else being in control of their physics temporarily. pObject->TakeDamage( CTakeDamageInfo( this, pOwner, 0, DMG_PHYSGUN ) ); Vector newPosition = start + forward * m_distance; // 24 is a little larger than 16 * sqrt(2) (extent of player bbox) // HACKHACK: We do this so we can "ignore" the player and the object we're manipulating // If we had a filter for tracelines, we could simply filter both ents and start from "start" Vector awayfromPlayer = start + forward * 24; UTIL_TraceLine( start, awayfromPlayer, MASK_SOLID, pOwner, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction == 1 ) { UTIL_TraceLine( awayfromPlayer, newPosition, MASK_SOLID, pObject, COLLISION_GROUP_NONE, &tr ); Vector dir = tr.endpos - newPosition; float distance = VectorNormalize(dir); float maxDist = m_gravCallback.m_maxVel * gpGlobals->frametime; if ( distance > maxDist ) { newPosition += dir * maxDist; } else { newPosition = tr.endpos; } } else { newPosition = tr.endpos; } CreatePelletAttraction( phys_gunglueradius.GetFloat(), pObject ); // If I'm looking more than 20 degrees away from the glue point, then give up // This lets the player "gesture" for the glue to let go. Vector pelletDir = m_gravCallback.m_worldPosition - start; VectorNormalize(pelletDir); if ( DotProduct( pelletDir, forward ) < 0.939 ) // 0.939 ~= cos(20deg) { // lose attach for 2 seconds if you're too far away m_glueTime = gpGlobals->curtime + 1; } if ( m_pelletHeld >= 0 && gpGlobals->curtime > m_glueTime ) { CGravityPellet *pPelletAttract = m_activePellets[m_pelletAttract].pellet; g_pEffects->Sparks( pPelletAttract->GetAbsOrigin() ); } m_gravCallback.SetTargetPosition( newPosition ); Vector dir = (newPosition - pObject->GetLocalOrigin()); m_movementLength = dir.Length(); } else { m_gravCallback.SetTargetPosition( end ); } if ( m_pelletHeld >= 0 && gpGlobals->curtime > m_glueTime ) { Vector worldNormal, worldPos; GetPelletWorldCoords( m_pelletAttract, &worldPos, &worldNormal ); m_gravCallback.SetAutoAlign( m_activePellets[m_pelletHeld].localNormal, m_activePellets[m_pelletHeld].pellet->GetLocalOrigin(), worldNormal, worldPos ); } else { m_gravCallback.ClearAutoAlign(); } } void CWeaponGravityGun::SoundCreate( void ) { m_soundState = SS_SCANNING; SoundStart(); } void CWeaponGravityGun::SoundDestroy( void ) { SoundStop(); } void CWeaponGravityGun::SoundStop( void ) { switch( m_soundState ) { case SS_SCANNING: GetOwner()->StopSound( "Weapon_Physgun.Scanning" ); break; case SS_LOCKEDON: GetOwner()->StopSound( "Weapon_Physgun.Scanning" ); GetOwner()->StopSound( "Weapon_Physgun.LockedOn" ); GetOwner()->StopSound( "Weapon_Physgun.LightObject" ); GetOwner()->StopSound( "Weapon_Physgun.HeavyObject" ); break; } } //----------------------------------------------------------------------------- // Purpose: returns the linear fraction of value between low & high (0.0 - 1.0) * scale // e.g. UTIL_LineFraction( 1.5, 1, 2, 1 ); will return 0.5 since 1.5 is // halfway between 1 and 2 // Input : value - a value between low & high (clamped) // low - the value that maps to zero // high - the value that maps to "scale" // scale - the output scale // Output : parametric fraction between low & high //----------------------------------------------------------------------------- static float UTIL_LineFraction( float value, float low, float high, float scale ) { if ( value < low ) value = low; if ( value > high ) value = high; float delta = high - low; if ( delta == 0 ) return 0; return scale * (value-low) / delta; } void CWeaponGravityGun::SoundStart( void ) { CPASAttenuationFilter filter( GetOwner() ); filter.MakeReliable(); switch( m_soundState ) { case SS_SCANNING: { EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.Scanning" ); } break; case SS_LOCKEDON: { // BUGBUG - If you start a sound with a pitch of 100, the pitch shift doesn't work! EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.LockedOn" ); EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.Scanning" ); EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.LightObject" ); EmitSound( filter, GetOwner()->entindex(), "Weapon_Physgun.HeavyObject" ); } break; } // volume, att, flags, pitch } void CWeaponGravityGun::SoundUpdate( void ) { int newState; if ( m_hObject ) newState = SS_LOCKEDON; else newState = SS_SCANNING; if ( newState != m_soundState ) { SoundStop(); m_soundState = newState; SoundStart(); } switch( m_soundState ) { case SS_SCANNING: break; case SS_LOCKEDON: { CPASAttenuationFilter filter( GetOwner() ); filter.MakeReliable(); float height = m_hObject->GetAbsOrigin().z - m_originalObjectPosition.z; // go from pitch 90 to 150 over a height of 500 int pitch = 90 + (int)UTIL_LineFraction( height, 0, 500, 60 ); CSoundParameters params; if ( GetParametersForSound( "Weapon_Physgun.LockedOn", params, NULL ) ) { EmitSound_t ep( params ); ep.m_nFlags = SND_CHANGE_VOL | SND_CHANGE_PITCH; ep.m_nPitch = pitch; EmitSound( filter, GetOwner()->entindex(), ep ); } // attenutate the movement sounds over 200 units of movement float distance = UTIL_LineFraction( m_movementLength, 0, 200, 1.0 ); // blend the "mass" sounds between 50 and 500 kg IPhysicsObject *pPhys = m_hObject->VPhysicsGetObject(); float fade = UTIL_LineFraction( pPhys->GetMass(), 50, 500, 1.0 ); if ( GetParametersForSound( "Weapon_Physgun.LightObject", params, NULL ) ) { EmitSound_t ep( params ); ep.m_nFlags = SND_CHANGE_VOL; ep.m_flVolume = fade * distance; EmitSound( filter, GetOwner()->entindex(), ep ); } if ( GetParametersForSound( "Weapon_Physgun.HeavyObject", params, NULL ) ) { EmitSound_t ep( params ); ep.m_nFlags = SND_CHANGE_VOL; ep.m_flVolume = (1.0 - fade) * distance; EmitSound( filter, GetOwner()->entindex(), ep ); } } break; } } void CWeaponGravityGun::AddPellet( CGravityPellet *pPellet, CBaseEntity *pAttach, const Vector &surfaceNormal ) { Assert(m_pelletCountIsInert() ) { if ( i != 0 ) { pelletlist_t tmp = m_activePellets[m_objectPelletCount]; m_activePellets[m_objectPelletCount] = m_activePellets[i]; m_activePellets[i] = tmp; } m_objectPelletCount++; } } SetObjectPelletsColor( 192, 255, 192 ); } void CWeaponGravityGun::SetObjectPelletsColor( int r, int g, int b ) { color32 color; color.r = r; color.g = g; color.b = b; color.a = 255; for ( int i = 0; i < m_objectPelletCount; i++ ) { CGravityPellet *pPellet = m_activePellets[i].pellet; if ( !pPellet || pPellet->IsInert() ) continue; pPellet->m_clrRender = color; } } CBaseEntity *CWeaponGravityGun::GetBeamEntity() { CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); if ( !pOwner ) return NULL; // Make sure I've got a view model CBaseViewModel *vm = pOwner->GetViewModel(); if ( vm ) return vm; return pOwner; } void CWeaponGravityGun::DeleteActivePellets() { CBaseEntity *pEnt = GetBeamEntity(); for ( int i = 0; i < m_pelletCount; i++ ) { CGravityPellet *pPellet = m_activePellets[i].pellet; if ( !pPellet ) continue; Vector forward; AngleVectors( pPellet->GetAbsAngles(), &forward ); g_pEffects->Dust( pPellet->GetAbsOrigin(), forward, 32, 30 ); // UNDONE: Probably should just do this client side CBeam *pBeam = CBeam::BeamCreate( PHYSGUN_BEAM_SPRITE, 1.5 ); pBeam->PointEntInit( pPellet->GetAbsOrigin(), pEnt ); pBeam->SetEndAttachment( 1 ); pBeam->SetBrightness( 255 ); pBeam->SetColor( 255, 0, 0 ); pBeam->RelinkBeam(); pBeam->LiveForTime( 0.1 ); UTIL_Remove( pPellet ); } m_pelletCount = 0; } void CWeaponGravityGun::CreatePelletAttraction( float radius, CBaseEntity *pObject ) { int nearPellet = -1; int objectPellet = -1; float best = radius*radius; // already have a pellet, check for in range if ( m_pelletAttract >= 0 ) { Vector attract, held; GetPelletWorldCoords( m_pelletAttract, &attract, NULL ); GetPelletWorldCoords( m_pelletHeld, &held, NULL ); float dist = (attract - held).Length(); if ( dist < radius * 2 ) { nearPellet = m_pelletAttract; objectPellet = m_pelletHeld; best = dist * dist; } } if ( nearPellet < 0 ) { for ( int i = 0; i < m_objectPelletCount; i++ ) { CGravityPellet *pPellet = m_activePellets[i].pellet; if ( !pPellet ) continue; for ( int j = m_objectPelletCount; j < m_pelletCount; j++ ) { CGravityPellet *pTest = m_activePellets[j].pellet; if ( !pTest ) continue; if ( pTest->IsInert() ) continue; float distSqr = (pTest->GetAbsOrigin() - pPellet->GetAbsOrigin()).LengthSqr(); if ( distSqr < best ) { Vector worldPos, worldNormal; GetPelletWorldCoords( j, &worldPos, &worldNormal ); // don't attract backside pellets (unless current pellet - prevent oscillation) float dist = DotProduct( worldPos, worldNormal ); if ( m_pelletAttract == j || DotProduct( pPellet->GetAbsOrigin(), worldNormal ) - dist >= 0 ) { best = distSqr; nearPellet = j; objectPellet = i; } } } } } m_glueTouching = false; if ( nearPellet < 0 || objectPellet < 0 ) { m_pelletAttract = -1; m_pelletHeld = -1; return; } if ( nearPellet != m_pelletAttract || objectPellet != m_pelletHeld ) { m_glueTime = gpGlobals->curtime; m_pelletAttract = nearPellet; m_pelletHeld = objectPellet; } // check for bonding if ( best < 3*3 ) { // This makes the pull towards the pellet stop getting stronger since some part of // the object is touching m_glueTouching = true; } } IPhysicsObject *CWeaponGravityGun::GetPelletPhysObject( int pelletIndex ) { if ( pelletIndex < 0 ) return NULL; CBaseEntity *pEntity = m_activePellets[pelletIndex].parent; if ( pEntity ) return pEntity->VPhysicsGetObject(); return g_PhysWorldObject; } void CWeaponGravityGun::EffectDestroy( void ) { m_active = false; SoundStop(); DetachObject(); } void CWeaponGravityGun::DetachObject( void ) { m_pelletHeld = -1; m_pelletAttract = -1; m_glueTouching = false; SetObjectPelletsColor( 255, 0, 0 ); m_objectPelletCount = 0; if ( m_hObject ) { CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); Pickup_OnPhysGunDrop( m_hObject, pOwner, DROPPED_BY_CANNON ); m_gravCallback.DetachEntity(); m_hObject = NULL; } } void CWeaponGravityGun::AttachObject( CBaseEntity *pObject, const Vector& start, const Vector &end, float distance ) { m_hObject = pObject; m_useDown = false; IPhysicsObject *pPhysics = pObject ? (pObject->VPhysicsGetObject()) : NULL; if ( pPhysics && pObject->GetMoveType() == MOVETYPE_VPHYSICS ) { m_distance = distance; m_gravCallback.AttachEntity( pObject, pPhysics, end ); float mass = pPhysics->GetMass(); Msg( "Object mass: %.2f lbs (%.2f kg)\n", kg2lbs(mass), mass ); float vel = phys_gunvel.GetFloat(); if ( mass > phys_gunmass.GetFloat() ) { vel = (vel*phys_gunmass.GetFloat())/mass; } m_gravCallback.SetMaxVelocity( vel ); // Msg( "Object mass: %.2f lbs (%.2f kg) %f %f %f\n", kg2lbs(mass), mass, pObject->GetAbsOrigin().x, pObject->GetAbsOrigin().y, pObject->GetAbsOrigin().z ); // Msg( "ANG: %f %f %f\n", pObject->GetAbsAngles().x, pObject->GetAbsAngles().y, pObject->GetAbsAngles().z ); m_originalObjectPosition = pObject->GetAbsOrigin(); m_pelletAttract = -1; m_pelletHeld = -1; pPhysics->Wake(); SortPelletsForObject( pObject ); CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); if( pOwner ) { Pickup_OnPhysGunPickup( pObject, pOwner ); } } else { m_hObject = NULL; } } //========================================================= //========================================================= void CWeaponGravityGun::PrimaryAttack( void ) { if ( !m_active ) { SendWeaponAnim( ACT_VM_PRIMARYATTACK ); EffectCreate(); SoundCreate(); } else { EffectUpdate(); SoundUpdate(); } } void CWeaponGravityGun::SecondaryAttack( void ) { m_flNextSecondaryAttack = gpGlobals->curtime + 0.1; if ( m_active ) { EffectDestroy(); SoundDestroy(); return; } CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); Assert( pOwner ); if ( pOwner->GetAmmoCount(m_iSecondaryAmmoType) <= 0 ) return; m_viewModelIndex = pOwner->entindex(); // Make sure I've got a view model CBaseViewModel *vm = pOwner->GetViewModel(); if ( vm ) { m_viewModelIndex = vm->entindex(); } Vector forward; pOwner->EyeVectors( &forward ); Vector start = pOwner->Weapon_ShootPosition(); Vector end = start + forward * 4096; trace_t tr; UTIL_TraceLine( start, end, MASK_SHOT, pOwner, COLLISION_GROUP_NONE, &tr ); if ( tr.fraction == 1.0 || (tr.surface.flags & SURF_SKY) ) return; CBaseEntity *pHit = tr.m_pEnt; if ( pHit->entindex() == 0 ) { pHit = NULL; } else { // if the object has no physics object, or isn't a physprop or brush entity, then don't glue if ( !pHit->VPhysicsGetObject() || pHit->GetMoveType() != MOVETYPE_VPHYSICS ) return; } QAngle angles; WeaponSound( SINGLE ); pOwner->RemoveAmmo( 1, m_iSecondaryAmmoType ); VectorAngles( tr.plane.normal, angles ); Vector endPoint = tr.endpos + tr.plane.normal; CGravityPellet *pPellet = (CGravityPellet *)CBaseEntity::Create( "gravity_pellet", endPoint, angles, this ); if ( pHit ) { pPellet->SetParent( pHit ); } AddPellet( pPellet, pHit, tr.plane.normal ); // UNDONE: Probably should just do this client side CBaseEntity *pEnt = GetBeamEntity(); CBeam *pBeam = CBeam::BeamCreate( PHYSGUN_BEAM_SPRITE, 1.5 ); pBeam->PointEntInit( endPoint, pEnt ); pBeam->SetEndAttachment( 1 ); pBeam->SetBrightness( 255 ); pBeam->SetColor( 255, 0, 0 ); pBeam->RelinkBeam(); pBeam->LiveForTime( 0.1 ); } void CWeaponGravityGun::WeaponIdle( void ) { if ( HasWeaponIdleTimeElapsed() ) { SendWeaponAnim( ACT_VM_IDLE ); if ( m_active ) { CBaseEntity *pObject = m_hObject; // pellet is touching object, so glue it if ( pObject && m_glueTouching ) { CGravityPellet *pPellet = m_activePellets[m_pelletAttract].pellet; if ( pPellet->MakeConstraint( pObject ) ) { WeaponSound( SPECIAL1 ); m_flNextPrimaryAttack = gpGlobals->curtime + 0.75; m_activePellets[m_pelletHeld].pellet->MakeInert(); } } EffectDestroy(); SoundDestroy(); } } } void CWeaponGravityGun::ItemPostFrame( void ) { CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); if (!pOwner) return; if ( pOwner->m_afButtonPressed & IN_ATTACK2 ) { SecondaryAttack(); } else if ( pOwner->m_nButtons & IN_ATTACK ) { PrimaryAttack(); } else if ( pOwner->m_afButtonPressed & IN_RELOAD ) { Reload(); } // ----------------------- // No buttons down // ----------------------- else { WeaponIdle( ); return; } } //----------------------------------------------------------------------------- // Purpose: // Output : Returns true on success, false on failure. //----------------------------------------------------------------------------- bool CWeaponGravityGun::HasAnyAmmo( void ) { //Always report that we have ammo return true; } //========================================================= //========================================================= bool CWeaponGravityGun::Reload( void ) { CBasePlayer *pOwner = ToBasePlayer( GetOwner() ); if ( pOwner->GetAmmoCount(m_iSecondaryAmmoType) != MAX_PELLETS ) { pOwner->SetAmmoCount( MAX_PELLETS, m_iSecondaryAmmoType ); DeleteActivePellets(); WeaponSound( RELOAD ); return true; } return false; } #define NUM_COLLISION_TESTS 2500 void CC_CollisionTest( const CCommand &args ) { if ( !physenv ) return; Msg( "Testing collision system\n" ); int i; CBaseEntity *pSpot = gEntList.FindEntityByClassname( NULL, "info_player_start"); Vector start = pSpot->GetAbsOrigin(); static Vector *targets = NULL; static bool first = true; static float test[2] = {1,1}; if ( first ) { targets = new Vector[NUM_COLLISION_TESTS]; float radius = 0; float theta = 0; float phi = 0; for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) { radius += NUM_COLLISION_TESTS * 123.123; radius = fabs(fmod(radius, 128)); theta += NUM_COLLISION_TESTS * 76.76; theta = fabs(fmod(theta, DEG2RAD(360))); phi += NUM_COLLISION_TESTS * 1997.99; phi = fabs(fmod(phi, DEG2RAD(180))); float st, ct, sp, cp; SinCos( theta, &st, &ct ); SinCos( phi, &sp, &cp ); targets[i].x = radius * ct * sp; targets[i].y = radius * st * sp; targets[i].z = radius * cp; // make the trace 1024 units long Vector dir = targets[i] - start; VectorNormalize(dir); targets[i] = start + dir * 1024; } first = false; } //Vector results[NUM_COLLISION_TESTS]; int testType = 0; if ( args.ArgC() >= 2 ) { testType = atoi( args[1] ); } float duration = 0; Vector size[2]; size[0].Init(0,0,0); size[1].Init(16,16,16); unsigned int dots = 0; for ( int j = 0; j < 2; j++ ) { float startTime = Plat_FloatTime(); if ( testType == 1 ) { const CPhysCollide *pCollide = g_PhysWorldObject->GetCollide(); trace_t tr; for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) { physcollision->TraceBox( start, targets[i], -size[j], size[j], pCollide, vec3_origin, vec3_angle, &tr ); dots += physcollision->ReadStat(0); //results[i] = tr.endpos; } } else { testType = 0; CBaseEntity *pWorld = GetContainingEntity( INDEXENT(0) ); trace_t tr; for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) { UTIL_TraceModel( start, targets[i], -size[j], size[j], pWorld, COLLISION_GROUP_NONE, &tr ); //results[i] = tr.endpos; } } duration += Plat_FloatTime() - startTime; } test[testType] = duration; Msg("%d collisions in %.2f ms (%u dots)\n", NUM_COLLISION_TESTS, duration*1000, dots ); Msg("Current speed ratio: %.2fX BSP:JGJK\n", test[1] / test[0] ); #if 0 int red = 255, green = 0, blue = 0; for ( i = 0; i < NUM_COLLISION_TESTS; i++ ) { NDebugOverlay::Line( start, results[i], red, green, blue, false, 2 ); } #endif } static ConCommand collision_test("collision_test", CC_CollisionTest, "Tests collision system", FCVAR_CHEAT );