/*
** Command & Conquer Renegade(tm)
** Copyright 2025 Electronic Arts Inc.
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see .
*/
/***********************************************************************************************
*** Confidential - Westwood Studios ***
***********************************************************************************************
* *
* Project Name : Commando *
* *
* $Archive:: /Commando/Code/Combat/bullet.cpp $*
* *
* $Author:: Byon_g $*
* *
* $Modtime:: 3/13/02 5:22p $*
* *
* $Revision:: 164 $*
* *
*---------------------------------------------------------------------------------------------*
* Functions: *
* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
#include "bullet.h"
#include "projectile.h"
#include "combat.h" // for collision groups (should be damage.h?)
#include "pscene.h"
#include "weaponmanager.h"
#include "assets.h"
#include "debug.h"
#include "armedgameobj.h"
#include "crandom.h"
#include "surfaceeffects.h"
#include "mesh.h"
#include "wwaudio.h"
#include "explosion.h"
#include "building.h"
#include "persistfactory.h"
#include "wwphysids.h"
#include "damageablestaticphys.h"
#include "wwprofile.h"
#include "part_emt.h"
#include "realcrc.h"
#include "soldier.h"
#include "segline.h"
#include "timeddecophys.h"
#include "simplegameobj.h"
#include "c4.h"
/*
** Constants for this module
*/
const float _INSTANT_BULLET_THRESHHOLD = 400; // If speed is >= _INSTANT_BULLET_THRESHHOLD m/s, treat it an an instant bullet
/**
** BeamEffectManagerClass
** This is a static object that simply manages the active beam effect. Its job is to cache un-used beams
** so that we aren't allocating them at run time and to add new beam effects to the scene when needed.
*/
class BeamEffectManagerClass : public CombatPhysObserverClass
{
public:
void Init( void ) {}
void Shutdown( void );
/*
** Create a beam effect and put it in the scene. Whenever possible, this function
** tries to recycle a previously created beam effect
*/
void Create_Beam_Effect(const AmmoDefinitionClass * ammo_def,const Vector3 & p0,const Vector3 & p1);
/*
** PhysObserver Interface - whenever a beam expires, we put it into a list so we can
** recycle it next time we need a beam effect
*/
virtual void Object_Removed_From_Scene(PhysClass * observed_obj);
private:
void Internal_Get_New_Beam(SegmentedLineClass ** beam_model,TimedDecorationPhysClass ** beam_wrapper);
RefMultiListClass UnusedBeamList;
};
static BeamEffectManagerClass _TheBeamEffectManager;
void BeamEffectManagerClass::Shutdown( void )
{
while (UnusedBeamList.Peek_Head()) {
UnusedBeamList.Release_Head();
}
}
void BeamEffectManagerClass::Create_Beam_Effect(const AmmoDefinitionClass * ammo_def,const Vector3 & p0,const Vector3 & p1)
{
SegmentedLineClass * beam_model = NULL;
TimedDecorationPhysClass * beam_wrapper = NULL;
Internal_Get_New_Beam(&beam_model,&beam_wrapper);
// line effect from "start" to "data.Position"
static Vector3 points[2];
points[0] = p0;
points[1] = p1;
const int BEAM_SUBDIVISION = 4;
const float BASE_AMPLITUDE = 0.3f;
const float AMPLITUDE_PER_METER = 0.04f;
/*
** User controlled settings
*/
TextureClass * beam_texture = Get_Texture_From_Filename(ammo_def->BeamTexture);
beam_model->Set_Texture(beam_texture);
REF_PTR_RELEASE(beam_texture);
beam_model->Set_Points(2,points);
beam_model->Set_Width(ammo_def->BeamWidth);
beam_model->Set_Color(ammo_def->BeamColor);
beam_model->Set_UV_Offset_Rate(Vector2(0,1.0f / ammo_def->BeamTime));
if (ammo_def->BeamSubdivisionEnabled) {
beam_model->Set_Noise_Amplitude((BASE_AMPLITUDE + AMPLITUDE_PER_METER * (p1-p0).Quick_Length()) * ammo_def->BeamSubdivisionScale);
beam_model->Set_Subdivision_Levels(BEAM_SUBDIVISION);
} else {
beam_model->Set_Noise_Amplitude(0.0f);
beam_model->Set_Subdivision_Levels(0);
}
/*
** Constants; I don't let the user control these for now
*/
beam_model->Set_Texture_Tile_Factor(1.0f); // Only relevant for TILED_TEXTURE_MAP mode
beam_model->Set_Texture_Mapping_Mode(SegLineRendererClass::UNIFORM_WIDTH_TEXTURE_MAP);
beam_model->Set_Opacity(1.0f);
beam_model->Set_Shader(ShaderClass::_PresetAdditiveShader);
beam_model->Set_Merge_Abort_Factor(1.0f);
beam_model->Set_Merge_Intersections(false);
beam_model->Set_Disable_Sorting(false);
beam_model->Set_Freeze_Random(ammo_def->BeamSubdivisionFrozen);
beam_model->Set_End_Caps(ammo_def->BeamEndCaps);
beam_model->Reset_Line();
beam_wrapper->Set_Lifetime(ammo_def->BeamTime);
beam_wrapper->Set_Transform(Matrix3D(1));
beam_wrapper->Enable_Is_Pre_Lit(true);
COMBAT_SCENE->Add_Dynamic_Object(beam_wrapper);
REF_PTR_RELEASE(beam_wrapper);
REF_PTR_RELEASE(beam_model);
REF_PTR_RELEASE(beam_texture);
}
void BeamEffectManagerClass::Object_Removed_From_Scene(PhysClass * observed_obj)
{
/*
** One of the "beams" that we are observing expired and was removed from the scene so
** lets put it in the list for future re-use
*/
UnusedBeamList.Add((TimedDecorationPhysClass *)observed_obj);
}
void BeamEffectManagerClass::Internal_Get_New_Beam(SegmentedLineClass ** set_beam_model,TimedDecorationPhysClass ** set_beam_wrapper)
{
if (UnusedBeamList.Is_Empty()) {
SegmentedLineClass * beam_model = NEW_REF(SegmentedLineClass,());
TimedDecorationPhysClass * beam_wrapper = NEW_REF(TimedDecorationPhysClass,());
beam_wrapper->Enable_Dont_Save(true);
beam_wrapper->Set_Collision_Group(UNCOLLIDEABLE_GROUP);
beam_wrapper->Set_Model(beam_model);
beam_wrapper->Set_Observer(this);
*set_beam_model = beam_model;
*set_beam_wrapper = beam_wrapper; // refs returned to the caller
} else {
*set_beam_wrapper = UnusedBeamList.Remove_Head(); // refs returned to the caller
*set_beam_model = (SegmentedLineClass *)((*set_beam_wrapper)->Get_Model());
}
}
/*
** Core Bullet Data for simulation
*/
class BulletDataClass {
public:
BulletDataClass( const AmmoDefinitionClass * def = NULL, const ArmedGameObj * owner = NULL,
const Vector3 & position = Vector3(0,0,0), const Vector3 & velocity = Vector3(0,0,0) ) :
AmmoDefinition( def ),
Owner( owner ),
Position( position ),
Velocity( velocity ),
SoftPierceCount( 0 ),
Destroy( false ),
DidExplode( false ),
LastHitID( 0 ),
GrenadeSafetyTimer( 0 )
{}
ArmedGameObj * Get_Owner( void ) const { return (ArmedGameObj *)Owner.Get_Ptr(); }
CollisionReactionType Bullet_Collision_Occurred( const CollisionEventClass & event );
ExpirationReactionType Bullet_Expired();
const AmmoDefinitionClass *AmmoDefinition;
GameObjReference Owner;
Vector3 Position;
Vector3 Velocity;
int SoftPierceCount;
bool Destroy;
bool DidExplode;
int LastHitID;
float GrenadeSafetyTimer;
};
/*
** Bullet_Collision_Occured
*/
CollisionReactionType BulletDataClass::Bullet_Collision_Occurred( const CollisionEventClass & event )
{
WWPROFILE( "Bullet Collision Occurred" );
WWASSERT(event.OtherObj);
WWASSERT(event.CollisionResult != NULL);
// WWDEBUG_SAY(( "Bullet Collision\n" ));
//
// Pre-calculate some useful variables
//
CollisionReactionType return_value = COLLISION_REACTION_DEFAULT;
OffenseObjectClass offense( AmmoDefinition->Damage, (int)AmmoDefinition->Warhead, Get_Owner() );
offense.EnableClientDamage = true; // Only bullets apply to client damage;
const Vector3 & collision_point = event.CollisionResult->ContactPoint;
const Vector3 & collision_normal = event.CollisionResult->Normal;
PhysicalGameObj * other = NULL;
BuildingGameObj * building = NULL;
if ( event.OtherObj->Get_Observer() != NULL ) {
other = ((CombatPhysObserverClass *)event.OtherObj->Get_Observer())->As_PhysicalGameObj();
building = ((CombatPhysObserverClass *)event.OtherObj->Get_Observer())->As_BuildingGameObj();
}
//
// If the bullet hits its owner or its owner's vehicle, allow it to continue
// Also, if it is a simple "Hidden Object", allow the bullet to continue
//
if ( other != NULL && Get_Owner() != NULL ) {
SimpleGameObj * simple = other->As_SimpleGameObj();
VehicleGameObj * vehicle = other->As_VehicleGameObj();
SoldierGameObj * soldier = Get_Owner()->As_SoldierGameObj();
if ( vehicle != NULL && soldier != NULL && vehicle == soldier->Get_Vehicle() ) {
// Debug_Say(( "Bullet Hitting its owner's vehicle\n" ));
return COLLISION_REACTION_NO_BOUNCE;
}
if ( other == Get_Owner() ) {
// Debug_Say(( "Bullet Hitting its owner\n" ));
return COLLISION_REACTION_NO_BOUNCE;
}
if ((simple != NULL) && (simple->Is_Hidden_Object())) {
return COLLISION_REACTION_NO_BOUNCE;
}
}
//
// Apply Graphical Effects.
// If the mesh is shatterable it is shattered, Apply a surface effect depending on the surface type
// that was collided with. In the case that the mesh was shattered, we disable surface effect decals.
//
bool shattered = false;
if ((event.CollidedRenderObj != NULL) && (event.CollidedRenderObj->Class_ID() == RenderObjClass::CLASSID_MESH)) {
MeshClass * mesh = (MeshClass *)event.CollidedRenderObj;
if ((mesh->Get_W3D_Flags() & W3D_MESH_FLAG_SHATTERABLE) && (mesh->Is_Not_Hidden_At_All())) {
PhysicsSceneClass::Get_Instance()->Shatter_Mesh( mesh,
collision_point,
collision_normal,
Velocity );
// remeber that we shattered the mesh so that we don't create decals later
shattered = true;
// Bullets always pass through shattered objects because we sometimes have two-layered
// windows that have different materials on the two sides. We don't want bullets to ever
// shatter only one "side" of a window!
return_value = COLLISION_REACTION_NO_BOUNCE;
}
}
// Ignore backfaces (after shattering for windows)
float dot = Vector3::Dot_Product( event.CollisionResult->Normal, Velocity );
if ( dot > 0 ) {
return COLLISION_REACTION_NO_BOUNCE;
}
//
// Apply Damage
// There are several types of objects that can be damaged: normal dynamic game objects, damageable static
// physics objects, and buildings. A building is damaged whenever any of its meshes or aggregates are
// hit by a bullet.
// Also, we only apply damage if the mesh wasn't shattered. Bullets pretend like nothing happened
// if they shatter something.
//
if (!shattered) {
const char * collided_obj_name = NULL;
if ( event.CollidedRenderObj != NULL ) {
collided_obj_name = event.CollidedRenderObj->Get_Name();
}
if ( other ) { // if hitting a game object,
if ( other->Get_ID() == LastHitID ) {
// Debug_Say(( "Double Hit\n" ));
return COLLISION_REACTION_NO_BOUNCE;
}
if ( GrenadeSafetyTimer > 0 ) {
// only if the other is not an enemy
if ( other && Get_Owner() && !other->Is_Enemy( Get_Owner() ) ) {
return COLLISION_REACTION_DEFAULT; // Just bounce
}
}
LastHitID = other->Get_ID();
// Apply Bullet Damage to a game object
// Allow it to happen for clients, so we can get the repair effect
// if ( CombatManager::I_Am_Server() ) {
other->Apply_Damage_Extended( offense, 1.0f, Velocity, collided_obj_name );
// If the bullet is hitting C4, also apply damage to the object the C4 is
// attached to. This is a fix for the "C4 armor" exploit.
if (other->As_C4GameObj() != NULL) {
ScriptableGameObj * host = other->As_C4GameObj()->Get_Stuck_Object();
if ((host != NULL) && (host->As_PhysicalGameObj() != NULL)) {
host->As_PhysicalGameObj()->Apply_Damage_Extended( offense, 1.0f, Velocity, NULL );
}
}
// }
if ( ( !other->Is_Soft() ) ||
( ++SoftPierceCount >= AmmoDefinition->SoftPierceLimit ) ) {
if ( AmmoDefinition->ExplosionDefID ) {
ExplosionManager::Create_Explosion_At( AmmoDefinition->ExplosionDefID, collision_point, Get_Owner(), -collision_normal, other );
}
Destroy = true;
return_value = COLLISION_REACTION_STOP_MOTION;
} else {
return_value = COLLISION_REACTION_NO_BOUNCE;
}
} else { // if hitting terain
if ( GrenadeSafetyTimer > 0 ) {
Debug_Say(( "Safety\n" ));
return COLLISION_REACTION_DEFAULT; // Just bounce
}
// If hitting a building, apply damage to it
if ( building != NULL ) { // && CombatManager::I_Am_Server() ) {
building->Apply_Damage_Building( offense, event.OtherObj->As_StaticPhysClass() );
}
// Check for damage to a DamageableStaticPhys object
// For now I'm using the persist factory chunk-ID for RTTI... If a better solution
// turns up we should change this.
if ( event.OtherObj->Get_Factory().Chunk_ID() == PHYSICS_CHUNKID_DAMAGEABLESTATICPHYS
// && CombatManager::I_Am_Server()
)
{
DamageableStaticPhysClass * damphys = (DamageableStaticPhysClass *)event.OtherObj;
OffenseObjectClass offense( AmmoDefinition->Damage, (int)AmmoDefinition->Warhead, Get_Owner() );
offense.EnableClientDamage = true; // Only bullets apply to client damage;
damphys->Apply_Damage_Static( offense );
}
// check for a non-stopping surface
if ( !SurfaceEffectsManager::Does_Surface_Stop_Bullets( event.CollisionResult->SurfaceType ) ) {
return_value = COLLISION_REACTION_NO_BOUNCE;
} else {
// If the ammo explodes when it hits terrain, then create an explosion and kill the bullet
if ( AmmoDefinition->TerrainActivated ) {
if ( AmmoDefinition->ExplosionDefID ) {
ExplosionManager::Create_Explosion_At( AmmoDefinition->ExplosionDefID, collision_point, Get_Owner(), -collision_normal, building );
DidExplode = true;
}
Destroy = true;
return_value = COLLISION_REACTION_STOP_MOTION;
}
}
}
}
// Only make a bullet hit if it doesn't make an explosion
if ( !DidExplode ) {
Matrix3D surface_tm;
surface_tm.Look_At(collision_point,collision_point-collision_normal,FreeRandom.Get_Float(0,DEG_TO_RADF(360)));
int my_type = AmmoDefinition->HitterType;
bool allow_decals = (shattered == false);
if ( (float)AmmoDefinition->Damage < 0.0f ) {
allow_decals = false;
}
SurfaceEffectsManager::Apply_Effect( event.CollisionResult->SurfaceType, my_type, surface_tm, event.OtherObj, Get_Owner(), allow_decals );
}
return return_value;
}
ExpirationReactionType BulletDataClass::Bullet_Expired( void )
{
Destroy = true;
if ( AmmoDefinition->TimeActivated && AmmoDefinition->ExplosionDefID && !DidExplode ) {
ExplosionManager::Create_Explosion_At( AmmoDefinition->ExplosionDefID, Position, Get_Owner() );
}
return EXPIRATION_DENIED; // Don't let it die, I'll pull it from the scene later
}
/*
** BulletClass (for non-instant bullets)
*/
class BulletClass : public CombatPhysObserverClass, public MultiListObjectClass, public PostLoadableClass {
public:
~BulletClass( void );
void Init( const BulletDataClass & data, float progress_time, const Vector3 & target, DamageableGameObj * target_object = NULL );
void Shutdown( void );
void Think( void );
// Save / Load
virtual bool Save( ChunkSaveClass & csave );
virtual bool Load( ChunkLoadClass & cload );
virtual void On_Post_Load(void);
bool Is_Valid( void ) { return BulletData.AmmoDefinition != NULL; }
// Collision
virtual CollisionReactionType Collision_Occurred( const CollisionEventClass & event );
virtual ExpirationReactionType Object_Expired( PhysClass * observed_obj );
private:
BulletDataClass BulletData;
ProjectileClass * Projectile; // for physics
Vector3 TargetVector;
GameObjReference TargetObject;
float TrackingErrorTimer;
Vector3 TrackingError;
long ModelNameCRC;
BulletClass( void );
friend class BulletManager;
};
BulletClass::BulletClass( void ) :
Projectile( NULL ),
TargetVector( 0,0,0 ),
TrackingErrorTimer( 0 ),
TrackingError( 0,0,0 ),
ModelNameCRC( 0 )
{
WWASSERT( Projectile == NULL );
Projectile = NEW_REF( ProjectileClass, () );
Projectile->Set_Collision_Group( BULLET_COLLISION_GROUP );
}
BulletClass::~BulletClass( void )
{
if ( Projectile != NULL ) {
Projectile->Set_Observer(NULL);
Projectile->Release_Ref();
Projectile = NULL;
}
}
void BulletClass::Init( const BulletDataClass & data, float progress_time, const Vector3 & target, DamageableGameObj * target_object )
{
WWPROFILE( "Bullet Init" );
BulletData.AmmoDefinition = data.AmmoDefinition;
BulletData.Owner = data.Get_Owner();
BulletData.SoftPierceCount = data.SoftPierceCount;
BulletData.Destroy = data.Destroy;
BulletData.LastHitID = 0; // We haven't hit anyone yet.
BulletData.DidExplode = false;
BulletData.GrenadeSafetyTimer = data.GrenadeSafetyTimer;
if ( data.AmmoDefinition ) {
BulletData.GrenadeSafetyTimer = data.AmmoDefinition->GrenadeSafetyTime;
}
TargetVector = target;
TargetObject = target_object;
TrackingErrorTimer = 0;
TrackingError = Vector3( 0,0,0 );
Projectile->Set_Observer( this );
Projectile->Set_Bounce_Count( BulletData.AmmoDefinition->MaxBounces );
if ( BulletData.AmmoDefinition ) {
Projectile->Set_Gravity_Multiplier( BulletData.AmmoDefinition->Gravity );
Projectile->Set_Elasticity( BulletData.AmmoDefinition->Elasticity );
}
Projectile->Enable_Is_Pre_Lit(true); // bullets don't get lighting.
if ( Projectile->Peek_Model() != NULL &&
Projectile->Peek_Model()->Get_Name() != NULL &&
BulletData.AmmoDefinition->ModelName.Compare_No_Case( Projectile->Peek_Model()->Get_Name() ) == 0 ) {
// We don't need to do anything
} else {
RenderObjClass * model = NULL;
model = WW3DAssetManager::Get_Instance ()->Create_Render_Obj( BulletData.AmmoDefinition->ModelName );
// If no name is given, lets create the NULL render obj
if ( model == NULL ) {
Debug_Say(( "Bullet Not Found \"%s\" \n", BulletData.AmmoDefinition->ModelName ));
model = WW3DAssetManager::Get_Instance ()->Create_Render_Obj( "NULL" );
}
Projectile->Set_Model( model );
if (model) {
if ( BulletData.AmmoDefinition->ModelName.Compare_No_Case( model->Get_Name() ) != 0 ) {
Debug_Say(( "Possible bullet twiddler!! %s %s\n", BulletData.AmmoDefinition->ModelName, model->Get_Name() ));
}
// ModelNameCRC = CRC_Stringi( model->Get_Name() );
ModelNameCRC = CRC_Stringi( BulletData.AmmoDefinition->ModelName );
model->Release_Ref();
}
else {
ModelNameCRC = 0xFFFFFFFF;
}
}
if (Projectile->Get_Gravity_Multiplier() < 0.1f) {
Projectile->Set_Orientation_Mode_Aligned_Fixed();
} else {
Projectile->Set_Orientation_Mode_Aligned();
}
COMBAT_SCENE->Add_Dynamic_Object( Projectile );
float duration = (float) BulletData.AmmoDefinition->Range / (float) BulletData.AmmoDefinition->Velocity;
if (duration <= 0.0) {
Debug_Say(( "NULL DURATION\n" ));
}
Projectile->Set_Lifetime( duration * 1.2f );
Projectile->Set_Position( data.Position );
WWASSERT(data.Velocity.Is_Valid());
Projectile->Set_Velocity( data.Velocity );
// If there are any emitters in this model, reset them for our new position
RenderObjClass * model = Projectile->Peek_Model();
model->Restart();
// Timestep last, incase it gets shutdown during this call
if ( data.Get_Owner() != NULL ) {
data.Get_Owner()->Peek_Physical_Object()->Inc_Ignore_Counter();
}
Projectile->Timestep( progress_time );
if ( data.Get_Owner() != NULL ) {
data.Get_Owner()->Peek_Physical_Object()->Dec_Ignore_Counter();
}
}
void BulletClass::Shutdown( void )
{
if ( Projectile != NULL ) {
COMBAT_SCENE->Remove_Object( Projectile );
Projectile->Set_Observer( NULL );
}
BulletData.AmmoDefinition = NULL;
BulletData.Owner = NULL;
}
/*
** BulletClass Save and Load
*/
enum {
CHUNKID_VARIABLES = 910991544,
CHUNKID_OWNER,
CHUNKID_TARGET_OBJECT,
MICROCHUNKID_AMMO_DEFINITION_ID = 1,
MICROCHUNKID_PROJECTILE,
MICROCHUNKID_SOFT_PIERCE_COUNT,
MICROCHUNKID_DESTROY,
MICROCHUNKID_TARGET_VECTOR,
MICROCHUNKID_TRACKING_ERROR_TIMER,
MICROCHUNKID_TRACKING_ERROR,
MICROCHUNKID_MODEL_NAME_CRC,
};
bool BulletClass::Save( ChunkSaveClass & csave )
{
csave.Begin_Chunk( CHUNKID_VARIABLES );
WWASSERT( BulletData.AmmoDefinition != NULL );
int def_id = BulletData.AmmoDefinition->Get_ID();
WRITE_MICRO_CHUNK( csave, MICROCHUNKID_AMMO_DEFINITION_ID, def_id );
WRITE_MICRO_CHUNK( csave, MICROCHUNKID_PROJECTILE, Projectile );
WRITE_MICRO_CHUNK( csave, MICROCHUNKID_SOFT_PIERCE_COUNT, BulletData.SoftPierceCount );
WRITE_MICRO_CHUNK( csave, MICROCHUNKID_DESTROY, BulletData.Destroy );
WRITE_MICRO_CHUNK( csave, MICROCHUNKID_TARGET_VECTOR, TargetVector );
WRITE_MICRO_CHUNK( csave, MICROCHUNKID_TRACKING_ERROR_TIMER, TrackingErrorTimer );
WRITE_MICRO_CHUNK( csave, MICROCHUNKID_TRACKING_ERROR, TrackingError );
WRITE_MICRO_CHUNK( csave, MICROCHUNKID_MODEL_NAME_CRC, ModelNameCRC );
csave.End_Chunk();
if ( BulletData.Get_Owner() != NULL ) {
csave.Begin_Chunk( CHUNKID_OWNER );
BulletData.Owner.Save( csave );
csave.End_Chunk();
}
if ( TargetObject.Get_Ptr() != NULL ) {
csave.Begin_Chunk( CHUNKID_TARGET_OBJECT );
TargetObject.Save( csave );
csave.End_Chunk();
}
return true;
}
bool BulletClass::Load( ChunkLoadClass & cload )
{
while (cload.Open_Chunk()) {
switch(cload.Cur_Chunk_ID()) {
case CHUNKID_VARIABLES:
{
int def_id = 0;
while (cload.Open_Micro_Chunk()) {
switch(cload.Cur_Micro_Chunk_ID()) {
READ_MICRO_CHUNK( cload, MICROCHUNKID_AMMO_DEFINITION_ID, def_id );
READ_MICRO_CHUNK( cload, MICROCHUNKID_PROJECTILE, Projectile );
READ_MICRO_CHUNK( cload, MICROCHUNKID_SOFT_PIERCE_COUNT, BulletData.SoftPierceCount );
READ_MICRO_CHUNK( cload, MICROCHUNKID_DESTROY, BulletData.Destroy );
READ_MICRO_CHUNK( cload, MICROCHUNKID_TARGET_VECTOR, TargetVector );
READ_MICRO_CHUNK( cload, MICROCHUNKID_TRACKING_ERROR_TIMER, TrackingErrorTimer );
READ_MICRO_CHUNK( cload, MICROCHUNKID_TRACKING_ERROR, TrackingError );
READ_MICRO_CHUNK( cload, MICROCHUNKID_MODEL_NAME_CRC, ModelNameCRC );
default:
Debug_Say(("Unhandled Micro Chunk:%d File:%s Line:%d\r\n",cload.Cur_Micro_Chunk_ID(),__FILE__,__LINE__));
break;
}
cload.Close_Micro_Chunk();
}
WWASSERT( BulletData.AmmoDefinition == NULL );
BulletData.AmmoDefinition = WeaponManager::Find_Ammo_Definition( def_id );
WWASSERT( BulletData.AmmoDefinition != NULL );
WWASSERT( Projectile != NULL );
if ( Projectile != NULL ) {
REQUEST_REF_COUNTED_POINTER_REMAP( (RefCountClass **)&Projectile );
}
break;
}
case CHUNKID_OWNER:
BulletData.Owner.Load( cload );
break;
case CHUNKID_TARGET_OBJECT:
TargetObject.Load( cload );
break;
default:
Debug_Say(("Unhandled Chunk:%d File:%s Line:%d\r\n",cload.Cur_Chunk_ID(),__FILE__,__LINE__));
break;
}
cload.Close_Chunk();
}
SaveLoadSystemClass::Register_Post_Load_Callback(this);
return true;
}
void BulletClass::On_Post_Load (void)
{
// Plug ourselves back into the physics object as an observer
WWASSERT(Projectile != NULL);
Projectile->Set_Observer(this);
return ;
}
CollisionReactionType BulletClass::Collision_Occurred( const CollisionEventClass & event )
{
// Copy the data from the Projectile
Projectile->Get_Velocity( &BulletData.Velocity );
Projectile->Get_Position( &BulletData.Position );
CollisionReactionType result = BulletData.Bullet_Collision_Occurred( event );
if ( BulletData.Destroy ) {
Shutdown();
}
return result;
}
ExpirationReactionType BulletClass::Object_Expired(PhysClass * observed_obj)
{
// Copy the data from the Projectile
Projectile->Get_Velocity( &BulletData.Velocity );
Projectile->Get_Position( &BulletData.Position );
ExpirationReactionType result = BulletData.Bullet_Expired();
if ( BulletData.Destroy ) {
Shutdown();
}
return result;
}
#define RANDOM_VECTOR( spread ) Vector3( FreeRandom.Get_Float( -(spread), (spread) ), \
FreeRandom.Get_Float( -(spread), (spread) ), \
FreeRandom.Get_Float( 0, (spread) ) )
void BulletClass::Think( void )
{
WWPROFILE( "Bullet Think" );
// Count down safety timer
BulletData.GrenadeSafetyTimer -= TimeManager::Get_Frame_Seconds();
if ( BulletData.GrenadeSafetyTimer < 0 ) {
BulletData.GrenadeSafetyTimer = 0;
}
// Handle Homing Bullets
if ( BulletData.AmmoDefinition->IsTracking ) {
// Track your target object
DamageableGameObj * target_obj = (DamageableGameObj *)TargetObject.Get_Ptr();
if ( target_obj != NULL ) {
if ( target_obj->As_PhysicalGameObj() ) {
TargetVector = target_obj->As_PhysicalGameObj()->Get_Bullseye_Position();
} else {
target_obj->Get_Position( &TargetVector );
}
}
Vector3 target_vector = TargetVector;
Vector3 obj_pos;
Projectile->Get_Transform().Get_Translation( &obj_pos );
// Debug_Say(( "Bullet Tracking %f %f %f\n", obj_pos.X, obj_pos.Y, obj_pos.Z ));
target_vector -= obj_pos;
// Find distance to target
float target_distance = target_vector.Length();
TrackingErrorTimer -= TimeManager::Get_Frame_Seconds();
if ( TrackingErrorTimer < 0 ) {
TrackingErrorTimer = FreeRandom.Get_Float( 0.1f, 0.4f );
TrackingError = RANDOM_VECTOR( BulletData.AmmoDefinition->RandomTrackingScale * target_distance / 2 );
}
target_vector += TrackingError;
target_vector.Normalize();
Vector3 current_vector;
Projectile->Get_Velocity( ¤t_vector );
current_vector.Normalize();
Vector3 cross = Vector3::Cross_Product( current_vector, target_vector );
// only re-aim if cross product not near zero
if ( cross.Length() > WWMATH_EPSILON ) {
float dot = Vector3::Dot_Product( target_vector, current_vector );
if ( WWMath::Fabs(dot) >= 1.0f ) {
// No turning needed
} else {
float angle = WWMath::Fast_Acos( dot );
float allowed_turn = BulletData.AmmoDefinition->TurnRate * TimeManager::Get_Frame_Seconds();
if ( angle > allowed_turn ) {
angle = allowed_turn;
}
cross.Normalize();
Matrix3D tm(cross,angle);
Vector3 current_vector;
Projectile->Get_Velocity( ¤t_vector );
WWASSERT(current_vector.Is_Valid());
current_vector = tm.Rotate_Vector( current_vector );
WWASSERT(current_vector.Is_Valid());
Projectile->Set_Velocity( current_vector );
}
}
}
}
/*
** Instant Bullet Code
*/
void Simulate_Instant_Bullet( BulletDataClass & data, float progress_time )
{
WWPROFILE("Simulate_Instant_Bullet");
// WWASSERT(data.Position.Is_Valid());
// WWASSERT(data.Velocity.Is_Valid());
Vector3 start = data.Position;
Vector3 end = data.Velocity;
end.Normalize();
end = start + (end * data.AmmoDefinition->Range);
// This is that last object we choose to ignore
PhysClass *blocker = NULL;
// Always ignore the owner
if ( data.Get_Owner() != NULL ) {
if ( data.Get_Owner()->Peek_Physical_Object() != NULL ) {
data.Get_Owner()->Peek_Physical_Object()->Inc_Ignore_Counter();
}
}
bool done = false;
int hit_counter = 0;
const int MAX_HITS = 5;
while ( !done && (hit_counter < MAX_HITS)) {
hit_counter++;
// Check for collisions in the path of the object
CastResultStruct res;
LineSegClass ray( data.Position, end );
PhysRayCollisionTestClass raytest(ray,&res,BULLET_COLLISION_GROUP,COLLISION_TYPE_PROJECTILE);
{WWPROFILE("Cast_Ray");
PhysicsSceneClass::Get_Instance()->Cast_Ray(raytest);
}
// If the result was a "startbad", we are done
if (raytest.Result->StartBad) {
// Debug_Say(( "Simulate_Instant_Bullet Startbad\n" ));
done = true;
} else {
if (raytest.Result->Fraction < 1.0f) {
// If there was a collision, set us at the point of collision
// Also, fill in the cast result struct.
ray.Compute_Point(raytest.Result->Fraction,&(raytest.Result->ContactPoint));
data.Position = raytest.Result->ContactPoint;
// Notify the parties involved
WWASSERT(raytest.CollidedPhysObj != NULL);
CollisionReactionType reaction = COLLISION_REACTION_DEFAULT;
CollisionEventClass event;
event.CollisionResult = raytest.Result;
event.CollidedRenderObj = raytest.CollidedRenderObj;
event.OtherObj = raytest.CollidedPhysObj;
reaction |= data.Bullet_Collision_Occurred(event);
if ( reaction & COLLISION_REACTION_NO_BOUNCE ) {
// We were requested to fly through. Mark the current blocker as ignore
// so we collide with him no more this pass
if ( blocker ) { // Stop ignoring the last blocker
blocker->Dec_Ignore_Counter();
}
blocker = raytest.CollidedPhysObj;
if ( blocker ) { // Start ignoring this blocker
blocker->Inc_Ignore_Counter();
}
} else {
done = true;
}
} else {
done = true;
data.Position = end;
}
}
}
if ( blocker ) { // Stop ignoring the last blocker
blocker->Dec_Ignore_Counter();
}
// stop ignoring the owner
if ( data.Get_Owner() != NULL ) {
if ( data.Get_Owner()->Peek_Physical_Object() != NULL ) {
data.Get_Owner()->Peek_Physical_Object()->Dec_Ignore_Counter();
}
}
// If not destroyed, Expire..
if ( !data.Destroy ) {
data.Bullet_Expired();
}
// If the definition requests a beam effect, launch it
if (data.AmmoDefinition->BeamEnabled) {
_TheBeamEffectManager.Create_Beam_Effect(data.AmmoDefinition,start,data.Position);
}
}
/*
** BulletManager
*/
MultiListClass LiveBulletList;
MultiListClass DeadBulletList;
void BulletManager::Init( void )
{
_TheBeamEffectManager.Init();
}
void BulletManager::Shutdown( void )
{
while ( !LiveBulletList.Is_Empty() ) {
delete LiveBulletList.Remove_Head();
}
while ( !DeadBulletList.Is_Empty() ) {
delete DeadBulletList.Remove_Head();
}
_TheBeamEffectManager.Shutdown();
}
void BulletManager::Update( void )
{
MultiListIterator it( &LiveBulletList );;
while (!it.Is_Done()) {
BulletClass * bullet = it.Peek_Obj();
if (bullet->BulletData.Destroy) {
bullet->Shutdown();
it.Remove_Current_Object();
DeadBulletList.Add( bullet );
} else {
bullet->Think();
it.Next();
}
}
}
#define BULLET_SPEED_CHEAT 0
void BulletManager::Create_Bullet( const AmmoDefinitionClass * def, const Vector3 & position,
const Vector3 & velocity, const ArmedGameObj * owner, float progress_time, const Vector3 & target,
DamageableGameObj * target_obj )
{
WWASSERT(velocity.Is_Valid());
BulletDataClass data( def, owner, position, velocity );
#if BULLET_SPEED_CHEAT
if ( (float)def->Velocity >= _INSTANT_BULLET_THRESHHOLD/3 ) {
#else
if ( (float)def->Velocity >= _INSTANT_BULLET_THRESHHOLD ) {
#endif
Simulate_Instant_Bullet( data, progress_time );
return;
}
WWPROFILE( "Create Bullet" );
BulletClass * bullet = NULL;
// Find a bullet
long crc = CRC_Stringi( def->ModelName );
MultiListIterator it( &DeadBulletList );;
while ( !it.Is_Done() && bullet == NULL ) {
BulletClass * test_bullet = it.Peek_Obj();
if ( test_bullet->ModelNameCRC == crc ) {
bullet = test_bullet;
it.Remove_Current_Object();
} else {
it.Next();
}
}
if ( bullet == NULL ) {
bullet = new BulletClass();
}
if ( bullet != NULL ) {
bullet->Init( data, progress_time, target, target_obj );
LiveBulletList.Add( bullet );
}
}
/*
** Save & Load
*/
enum {
CHUNKID_BULLET = 916991653,
};
bool BulletManager::Save( ChunkSaveClass &csave )
{
MultiListIterator it( &LiveBulletList );;
while (!it.Is_Done()) {
BulletClass * bullet = it.Peek_Obj();
// Only save valid bullets
if ( bullet->Is_Valid() ) {
csave.Begin_Chunk( CHUNKID_BULLET );
bullet->Save( csave );
csave.End_Chunk();
}
it.Next();
}
return true;
}
bool BulletManager::Load( ChunkLoadClass &cload )
{
while (cload.Open_Chunk()) {
switch(cload.Cur_Chunk_ID()) {
case CHUNKID_BULLET:
{
BulletClass * bullet = new BulletClass();
bullet->Load( cload );
LiveBulletList.Add( bullet );
break;
}
default:
Debug_Say(( "Unrecognized BulletManager chunkID %d\n", cload.Cur_Chunk_ID() ));
break;
}
cload.Close_Chunk();
}
return true;
}