//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" #include "core/stream/bitStream.h" #include "console/console.h" #include "console/consoleTypes.h" #include "console/engineAPI.h" #include "sim/netConnection.h" #include "math/mMath.h" #include "math/mathIO.h" #include "math/mathUtils.h" #include "gfx/gfxTransformSaver.h" #include "gfx/gfxDrawUtil.h" #include "sfx/sfxTrack.h" #include "sfx/sfxTypes.h" #include "sfx/sfxSystem.h" #include "ts/tsShapeInstance.h" #include "T3D/fx/explosion.h" #include "T3D/proximityMine.h" #include "T3D/physics/physicsBody.h" const U32 sTriggerCollisionMask = ( VehicleObjectType | PlayerObjectType ); static S32 gAutoDeleteTicks = 16; // Provides about half a second for all clients to be updated // before a mine is deleted on the server. //---------------------------------------------------------------------------- IMPLEMENT_CO_DATABLOCK_V1( ProximityMineData ); ConsoleDocClass( ProximityMineData, "@brief Stores common properties for a ProximityMine.\n\n" "@see ProximityMine\n" "@ingroup gameObjects\n" ); IMPLEMENT_CALLBACK( ProximityMineData, onTriggered, void, ( ProximityMine* obj, SceneObject *target ),( obj, target ), "Callback invoked when an object triggers the ProximityMine.\n\n" "@param obj The ProximityMine object\n" "@param target The object that triggered the mine\n" "@note This callback is only invoked on the server.\n" "@see ProximityMine\n" ); IMPLEMENT_CALLBACK( ProximityMineData, onExplode, void, ( ProximityMine* obj, Point3F pos ),( obj, pos ), "Callback invoked when a ProximityMine is about to explode.\n\n" "@param obj The ProximityMine object\n" "@param pos The position of the mine explosion\n" "@note This callback is only invoked on the server.\n" "@see ProximityMine\n" ); ProximityMineData::ProximityMineData() : armingDelay( 0 ), armingSequence( -1 ), triggerRadius( 5.0f ), triggerSpeed( 1.0f ), autoTriggerDelay( 0 ), triggerOnOwner( false ), triggerDelay( 0 ), triggerSequence( -1 ), explosionOffset( 0.05f ) { INIT_ASSET(ArmSound); INIT_ASSET(TriggerSound); } void ProximityMineData::initPersistFields() { docsURL; Parent::initPersistFields(); addGroup("Sounds"); INITPERSISTFIELD_SOUNDASSET(ArmSound, ProximityMineData, "Arming sound for this proximity mine."); INITPERSISTFIELD_SOUNDASSET(TriggerSound, ProximityMineData, "Arming sound for this proximity mine."); endGroup("Sounds"); addGroup( "Arming" ); addField( "armingDelay", TypeF32, Offset(armingDelay, ProximityMineData), "Delay (in seconds) from when the mine is placed to when it becomes active." ); endGroup( "Arming" ); addGroup( "Triggering" ); addField( "autoTriggerDelay", TypeF32, Offset(autoTriggerDelay, ProximityMineData), "@brief Delay (in seconds) from arming until the mine automatically " "triggers and explodes, even if no object has entered the trigger area.\n\n" "Set to 0 to disable." ); addField( "triggerOnOwner", TypeBool, Offset(triggerOnOwner, ProximityMineData), "@brief Controls whether the mine can be triggered by the object that owns it.\n\n" "For example, a player could deploy mines that are only dangerous to other " "players and not himself." ); addField( "triggerRadius", TypeF32, Offset(triggerRadius, ProximityMineData), "Distance at which an activated mine will detect other objects and explode." ); addField( "triggerSpeed", TypeF32, Offset(triggerSpeed, ProximityMineData), "Speed above which moving objects within the trigger radius will trigger the mine" ); addField( "triggerDelay", TypeF32, Offset(triggerDelay, ProximityMineData), "Delay (in seconds) from when the mine is triggered until it explodes." ); endGroup( "Triggering" ); addGroup( "Explosion" ); addField( "explosionOffset", TypeF32, Offset(explosionOffset, ProximityMineData), "@brief Offset from the mine's origin where the explosion emanates from." "Sometimes a thrown mine may be slightly sunk into the ground. This can be just " "enough to cause the explosion to occur under the ground, especially on flat " "ground, which can end up blocking the explosion. This offset along the mine's " "'up' normal allows you to raise the explosion origin to a better height."); endGroup( "Explosion" ); } bool ProximityMineData::preload( bool server, String& errorStr ) { if ( Parent::preload( server, errorStr ) == false ) return false; if ( !server ) { if( !getArmSound() ) Con::errorf( ConsoleLogEntry::General, "ProximityMineData::preload: Invalid arming sound." ); if( !getTriggerSound() ) Con::errorf( ConsoleLogEntry::General, "ProximityMineData::preload: Invalid trigger sound." ); } if ( mShape ) { // Lookup animation sequences armingSequence = mShape->findSequence( "armed" ); triggerSequence = mShape->findSequence( "triggered" ); } return true; } void ProximityMineData::packData( BitStream* stream ) { Parent::packData( stream ); stream->write( armingDelay ); PACKDATA_ASSET(ArmSound); stream->write( autoTriggerDelay ); stream->writeFlag( triggerOnOwner ); stream->write( triggerRadius ); stream->write( triggerSpeed ); stream->write( triggerDelay ); PACKDATA_ASSET(TriggerSound); } void ProximityMineData::unpackData( BitStream* stream ) { Parent::unpackData(stream); stream->read( &armingDelay ); UNPACKDATA_ASSET(ArmSound); stream->read( &autoTriggerDelay ); triggerOnOwner = stream->readFlag(); stream->read( &triggerRadius ); stream->read( &triggerSpeed ); stream->read( &triggerDelay ); UNPACKDATA_ASSET(TriggerSound); } //---------------------------------------------------------------------------- IMPLEMENT_CO_NETOBJECT_V1( ProximityMine ); ConsoleDocClass( ProximityMine, "@brief A simple proximity mine.\n\n" "Proximity mines can be deployed using the world editor or thrown by an " "in-game object. Once armed, any Player or Vehicle object that moves within " "the mine's trigger area will cause it to explode.\n\n" "Internally, the ProximityMine object transitions through the following states:\n" "
    \n" "
  1. Thrown: Mine has been thrown, but has not yet attached to a surface
  2. \n" "
  3. Deployed: Mine has attached to a surface but is not yet armed. Start " "playing the #armingSound and armed sequence.
  4. \n" "
  5. Armed: Mine is armed and will trigger if a Vehicle or Player object moves " "within the trigger area.
  6. \n" "
  7. Triggered: Mine has been triggered and will explode soon. Invoke the " "onTriggered callback, and start playing the #triggerSound and triggered " "sequence.
  8. \n" "
  9. Exploded: Mine has exploded and will be deleted on the server shortly. " "Invoke the onExplode callback on the server and generate the explosion effects " "on the client.
  10. \n" "
\n\n" "@note Proximity mines with the #static field set to true will start in the " "Armed state. Use this for mines placed with the World Editor.\n\n" "The shape used for the mine may optionally define the following sequences:\n" "
\n" "
armed
Sequence to play when the mine is deployed, but before " "it becomes active and triggerable (#armingDelay should be set appropriately).
\n" "
triggered
Sequence to play when the mine is triggered, just " "before it explodes (#triggerDelay should be set appropriately).
\n" "
\n\n" "@tsexample\n" "datablock ProximityMineData( SimpleMine )\n" "{\n" " // ShapeBaseData fields\n" " category = \"Weapon\";\n" " shapeFile = \"art/shapes/weapons/misc/proximityMine.dts\";\n\n" " // ItemData fields\n" " sticky = true;\n\n" " // ProximityMineData fields\n" " armingDelay = 0.5;\n" " armingSound = MineArmedSound;\n\n" " autoTriggerDelay = 0;\n" " triggerOnOwner = true;\n" " triggerRadius = 5.0;\n" " triggerSpeed = 1.0;\n" " triggerDelay = 0.5;\n" " triggerSound = MineTriggeredSound;\n" " explosion = RocketLauncherExplosion;\n\n" " // dynamic fields\n" " pickUpName = \"Proximity Mines\";\n" " maxInventory = 20;\n\n" " damageType = \"MineDamage\"; // type of damage applied to objects in radius\n" " radiusDamage = 30; // amount of damage to apply to objects in radius\n" " damageRadius = 8; // search radius to damage objects when exploding\n" " areaImpulse = 2000; // magnitude of impulse to apply to objects in radius\n" "};\n\n" "function ProximityMineData::onTriggered( %this, %obj, %target )\n" "{\n" " echo( %this.name SPC \"triggered by \" @ %target.getClassName() );\n" "}\n\n" "function ProximityMineData::onExplode( %this, %obj, %position )\n" "{\n" " // Damage objects within the mine's damage radius\n" " if ( %this.damageRadius > 0 )\n" " radiusDamage( %obj.sourceObject, %position, %this.damageRadius, %this.radiusDamage, %this.damageType, %this.areaImpulse );\n" "}\n\n" "function ProximityMineData::damage( %this, %obj, %position, %source, %amount, %damageType )\n" "{\n" " // Explode if any damage is applied to the mine\n" " %obj.schedule(50 + getRandom(50), explode);\n" "}\n\n" "%obj = new ProximityMine()\n" "{\n" " dataBlock = SimpleMine;\n" "};\n" "@endtsexample\n\n" "@see ProximityMineData\n" "@ingroup gameObjects\n" ); ProximityMine::ProximityMine() { mTypeMask |= StaticShapeObjectType; mDataBlock = 0; mStickyCollisionPos.zero(); mOwner = NULL; mState = Thrown; mStateTimeout = 0; mAnimThread = NULL; // For the Item class mSubclassItemHandlesScene = true; } ProximityMine::~ProximityMine() { } //---------------------------------------------------------------------------- void ProximityMine::consoleInit() { Parent::consoleInit(); Con::addVariable("$ProxMine::autoDeleteTicks", TypeS32, &gAutoDeleteTicks, "@brief Number of ticks until an exploded mine is deleted on the server.\n\n" "After a mine has exploded it remains in the server's scene graph for a time " "to allow its exploded state to be passed along to each client. This variable " "controls how long a mine remains before it is deleted. Any client that has not " "received the exploded state by then (perhaps due to lag) will not see any " "explosion produced by the mine.\n\n" "@ingroup GameObjects"); } //---------------------------------------------------------------------------- bool ProximityMine::onAdd() { if ( !Parent::onAdd() || !mDataBlock ) return false; addToScene(); if (isServerObject()) scriptOnAdd(); if ( mStatic ) { // static mines are armed immediately mState = Deployed; mStateTimeout = 0; } return true; } bool ProximityMine::onNewDataBlock( GameBaseData* dptr, bool reload ) { mDataBlock = dynamic_cast( dptr ); if ( !mDataBlock || !Parent::onNewDataBlock( dptr, reload ) ) return false; scriptOnNewDataBlock(); return true; } void ProximityMine::onRemove() { scriptOnRemove(); removeFromScene(); Parent::onRemove(); } //---------------------------------------------------------------------------- void ProximityMine::setTransform( const MatrixF& mat ) { ShapeBase::setTransform( mat ); // Skip Item::setTransform as it restricts rotation to the Z axis if ( !mStatic ) { mAtRest = false; mAtRestCounter = 0; } if ( mPhysicsRep ) mPhysicsRep->setTransform( getTransform() ); setMaskBits( Item::RotationMask | Item::PositionMask | Item::NoWarpMask ); } void ProximityMine::setDeployedPos( const Point3F& pos, const Point3F& normal ) { // Align to deployed surface normal MatrixF mat( true ); MathUtils::getMatrixFromUpVector( normal, &mat ); mat.setPosition( pos + normal * mObjBox.minExtents.z ); mDelta.pos = pos; mDelta.posVec.set(0, 0, 0); ShapeBase::setTransform( mat ); if ( mPhysicsRep ) mPhysicsRep->setTransform( getTransform() ); setMaskBits( DeployedMask ); } void ProximityMine::processTick( const Move* move ) { Parent::processTick( move ); // Process state machine mStateTimeout -= TickSec; State lastState = NumStates;; while ( mState != lastState ) { lastState = mState; switch ( mState ) { case Thrown: if ( mAtRest ) { mState = Deployed; mStateTimeout = mDataBlock->armingDelay; // Get deployed position if mine was not stuck to another surface if ( mStickyCollisionPos.isZero() ) { mObjToWorld.getColumn( 2, &mStickyCollisionNormal ); mObjToWorld.getColumn( 3, &mStickyCollisionPos ); } setDeployedPos( mStickyCollisionPos, mStickyCollisionNormal ); if ( mDataBlock->armingSequence != -1 ) { mAnimThread = mShapeInstance->addThread(); mShapeInstance->setSequence( mAnimThread, mDataBlock->armingSequence, 0.0f ); } if ( mDataBlock->getArmSoundProfile() ) SFX->playOnce( mDataBlock->getArmSoundProfile(), &getRenderTransform() ); } break; case Deployed: // Timeout into Armed state if ( mStateTimeout <= 0 ) { mState = Armed; mStateTimeout = mDataBlock->autoTriggerDelay ? mDataBlock->autoTriggerDelay : F32_MAX; } break; case Armed: { // Check for objects within the trigger area Box3F triggerBox( mDataBlock->triggerRadius * 2 ); triggerBox.setCenter( getTransform().getPosition() ); SimpleQueryList sql; getContainer()->findObjects( triggerBox, sTriggerCollisionMask, SimpleQueryList::insertionCallback, &sql ); for ( S32 i = 0; i < sql.mList.size(); i++ ) { // Detect movement in the trigger area if ( ( sql.mList[i] == mOwner && !mDataBlock->triggerOnOwner ) || sql.mList[i]->getVelocity().len() < mDataBlock->triggerSpeed ) continue; // Mine has been triggered mShapeInstance->destroyThread( mAnimThread ); mAnimThread = NULL; mState = Triggered; mStateTimeout = mDataBlock->triggerDelay; if ( mDataBlock->triggerSequence != -1 ) { mAnimThread = mShapeInstance->addThread(); mShapeInstance->setSequence( mAnimThread, mDataBlock->triggerSequence, 0.0f ); } if ( mDataBlock->getTriggerSoundProfile() ) SFX->playOnce( mDataBlock->getTriggerSoundProfile(), &getRenderTransform() ); if ( isServerObject() ) mDataBlock->onTriggered_callback( this, sql.mList[0] ); } break; } case Triggered: // Timeout into exploded state if ( mStateTimeout <= 0 ) { explode(); } break; case Exploded: // Mine's delete themselves on the server after exploding if ( isServerObject() && ( mStateTimeout <= 0 ) ) { deleteObject(); return; } break; } } } //---------------------------------------------------------------------------- void ProximityMine::explode() { // Make sure we don't explode twice if ( mState == Exploded ) { return; } mState = Exploded; mStateTimeout = TickSec * gAutoDeleteTicks; // auto-delete on server N ticks after exploding // Move the explosion point slightly off the surface to avoid problems with radius damage Point3F normal = getTransform().getUpVector(); Point3F explodePos = getTransform().getPosition() + normal * mDataBlock->explosionOffset; if ( isServerObject() ) { // Do what the server needs to do, damage the surrounding objects, etc. mDataBlock->onExplode_callback( this, explodePos ); setMaskBits( ExplosionMask ); // Wait till the timeout to self delete. This gives the server object time // to get ghosted to the client } else { // Client just plays the explosion effect at the right place if ( mDataBlock->explosion ) { Explosion *pExplosion = new Explosion; pExplosion->onNewDataBlock( mDataBlock->explosion, false ); MatrixF xform( true ); xform.setPosition( explodePos ); pExplosion->setTransform( xform ); pExplosion->setInitialState( explodePos, normal ); pExplosion->setCollideType( sTriggerCollisionMask ); if ( pExplosion->registerObject() == false ) { Con::errorf( ConsoleLogEntry::General, "ProximityMine(%s)::explode: couldn't register explosion", mDataBlock->getName() ); delete pExplosion; } } } } void ProximityMine::advanceTime( F32 dt ) { Parent::advanceTime( dt ); if ( mAnimThread ) mShapeInstance->advancePos( dt, mAnimThread ); } //---------------------------------------------------------------------------- U32 ProximityMine::packUpdate( NetConnection* connection, U32 mask, BitStream* stream ) { // Handle rotation ourselves (so it is not locked to the Z axis like for Items) U32 retMask = Parent::packUpdate( connection, mask & (~Item::RotationMask), stream ); if ( stream->writeFlag( mask & Item::RotationMask ) ) { QuatF rot( mObjToWorld ); mathWrite( *stream, rot ); } if ( stream->writeFlag( !mStatic && ( mask & DeployedMask ) && ( mState > Thrown ) ) ) { mathWrite( *stream, mStickyCollisionPos ); mathWrite( *stream, mStickyCollisionNormal ); } stream->writeFlag( ( mask & ExplosionMask ) && ( mState == Exploded ) ); return retMask; } void ProximityMine::unpackUpdate( NetConnection* connection, BitStream* stream ) { Parent::unpackUpdate( connection, stream ); // Item::RotationMask if ( stream->readFlag() ) { QuatF rot; mathRead( *stream, &rot ); Point3F pos = mObjToWorld.getPosition(); rot.setMatrix( &mObjToWorld ); mObjToWorld.setPosition( pos ); } // !mStatic && ( mask & DeployedMask ) && ( mState > Thrown ) if ( stream->readFlag() ) { mathRead( *stream, &mStickyCollisionPos ); mathRead( *stream, &mStickyCollisionNormal ); mAtRest = true; setDeployedPos( mStickyCollisionPos, mStickyCollisionNormal ); } // ( mask & ExplosionMask ) && ( mState == Exploded ) if ( stream->readFlag() ) { // start the explosion visuals on the client explode(); } if ( mStatic && mState <= Deployed ) { // static mines are armed immediately mState = Deployed; mStateTimeout = 0; } } //---------------------------------------------------------------------------- void ProximityMine::prepRenderImage( SceneRenderState* state ) { // Don't render the mine if exploded if ( mState == Exploded ) return; // Use ShapeBase to render the 3D shape Parent::prepRenderImage( state ); // Add a custom render instance to draw the trigger area if ( !state->isShadowPass() ) { ObjectRenderInst* ri = state->getRenderPass()->allocInst(); ri->renderDelegate.bind( this, &ProximityMine::renderObject ); ri->type = RenderPassManager::RIT_ObjectTranslucent; ri->translucentSort = true; ri->defaultKey = 1; state->getRenderPass()->addInst( ri ); } } void ProximityMine::renderObject( ObjectRenderInst* ri, SceneRenderState* state, BaseMatInstance* overrideMat ) { if ( overrideMat ) return; /* // Render the trigger area if ( mState == Armed || mState == Triggered ) { const LinearColorF drawColor(1, 0, 0, 0.05f); if ( drawColor.alpha > 0 ) { GFXStateBlockDesc desc; desc.setZReadWrite( true, false ); desc.setBlend( true ); GFXTransformSaver saver; MatrixF mat = getRenderTransform(); mat.scale( getScale() ); GFX->getDrawUtil()->drawSphere( desc, mDataBlock->triggerRadius, mat.getPosition(), drawColor, true, false, &mat ); } } */ } //---------------------------------------------------------------------------- DefineEngineMethod( ProximityMine, explode, void, (),, "@brief Manually cause the mine to explode.\n\n") { object->explode(); }