/*
** Command & Conquer Generals Zero Hour(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 .
*/
////////////////////////////////////////////////////////////////////////////////
// //
// (c) 2001-2003 Electronic Arts Inc. //
// //
////////////////////////////////////////////////////////////////////////////////
// FILE: DumbProjectileBehavior.cpp
// Author: Steven Johnson, July 2002
// Desc:
#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine
#include "Common/BezierSegment.h"
#include "Common/GameCommon.h"
#include "Common/GameState.h"
#include "Common/Player.h"
#include "Common/ThingTemplate.h"
#include "Common/RandomValue.h"
#include "Common/Xfer.h"
#include "GameClient/Drawable.h"
#include "GameClient/FXList.h"
#include "GameLogic/GameLogic.h"
#include "GameLogic/Object.h"
#include "GameLogic/PartitionManager.h"
#include "GameLogic/Module/ContainModule.h"
#include "GameLogic/Module/DumbProjectileBehavior.h"
#include "GameLogic/Module/MissileAIUpdate.h"
#include "GameLogic/Module/PhysicsUpdate.h"
#include "GameLogic/Weapon.h"
#ifdef _INTERNAL
// for occasional debugging...
//#pragma optimize("", off)
//#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes")
#endif
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
const Int DEFAULT_MAX_LIFESPAN = 10 * LOGICFRAMES_PER_SECOND;
//-----------------------------------------------------------------------------
DumbProjectileBehaviorModuleData::DumbProjectileBehaviorModuleData() :
m_maxLifespan(DEFAULT_MAX_LIFESPAN),
m_detonateCallsKill(FALSE),
m_orientToFlightPath(TRUE),
m_tumbleRandomly(FALSE),
m_firstHeight(0.0f),
m_secondHeight(0.0f),
m_firstPercentIndent(0.0f),
m_secondPercentIndent(0.0f),
m_garrisonHitKillCount(0),
m_garrisonHitKillFX(NULL),
m_flightPathAdjustDistPerFrame(0.0f)
{
}
//-----------------------------------------------------------------------------
void DumbProjectileBehaviorModuleData::buildFieldParse(MultiIniFieldParse& p)
{
UpdateModuleData::buildFieldParse(p);
static const FieldParse dataFieldParse[] =
{
{ "MaxLifespan", INI::parseDurationUnsignedInt, NULL, offsetof( DumbProjectileBehaviorModuleData, m_maxLifespan ) },
{ "TumbleRandomly", INI::parseBool, NULL, offsetof( DumbProjectileBehaviorModuleData, m_tumbleRandomly ) },
{ "DetonateCallsKill", INI::parseBool, NULL, offsetof( DumbProjectileBehaviorModuleData, m_detonateCallsKill ) },
{ "OrientToFlightPath", INI::parseBool, NULL, offsetof( DumbProjectileBehaviorModuleData, m_orientToFlightPath ) },
{ "FirstHeight", INI::parseReal, NULL, offsetof( DumbProjectileBehaviorModuleData, m_firstHeight ) },
{ "SecondHeight", INI::parseReal, NULL, offsetof( DumbProjectileBehaviorModuleData, m_secondHeight ) },
{ "FirstPercentIndent", INI::parsePercentToReal, NULL, offsetof( DumbProjectileBehaviorModuleData, m_firstPercentIndent ) },
{ "SecondPercentIndent", INI::parsePercentToReal, NULL, offsetof( DumbProjectileBehaviorModuleData, m_secondPercentIndent ) },
{ "GarrisonHitKillRequiredKindOf", KindOfMaskType::parseFromINI, NULL, offsetof( DumbProjectileBehaviorModuleData, m_garrisonHitKillKindof ) },
{ "GarrisonHitKillForbiddenKindOf", KindOfMaskType::parseFromINI, NULL, offsetof( DumbProjectileBehaviorModuleData, m_garrisonHitKillKindofNot ) },
{ "GarrisonHitKillCount", INI::parseUnsignedInt, NULL, offsetof( DumbProjectileBehaviorModuleData, m_garrisonHitKillCount ) },
{ "GarrisonHitKillFX", INI::parseFXList, NULL, offsetof( DumbProjectileBehaviorModuleData, m_garrisonHitKillFX ) },
{ "FlightPathAdjustDistPerSecond", INI::parseVelocityReal, NULL, offsetof( DumbProjectileBehaviorModuleData, m_flightPathAdjustDistPerFrame ) },
{ 0, 0, 0, 0 }
};
p.add(dataFieldParse);
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
DumbProjectileBehavior::DumbProjectileBehavior( Thing *thing, const ModuleData* moduleData ) : UpdateModule( thing, moduleData )
{
m_launcherID = INVALID_ID;
m_victimID = INVALID_ID;
m_detonationWeaponTmpl = NULL;
m_lifespanFrame = 0;
m_flightPath.clear();
m_flightPathSegments = 0;
m_flightPathSpeed = 0;
m_flightPathStart.zero();
m_flightPathEnd.zero();
m_currentFlightPathStep = 0;
m_extraBonusFlags = 0;
m_hasDetonated = FALSE;
}
//-------------------------------------------------------------------------------------------------
DumbProjectileBehavior::~DumbProjectileBehavior()
{
}
//-------------------------------------------------------------------------------------------------
inline Bool within(Real min, Real val, Real max)
{
return min <= val && val <= max;
}
//-------------------------------------------------------------------------------------------------
inline Bool notWithin(Real min, Real val, Real max)
{
return val < min || val > max;
}
#ifdef NOT_IN_USE
#define NO_ONLY_RETURN_CLIPPED_PITCHES
//-------------------------------------------------------------------------------------------------
static Bool calcTrajectory(
const Coord3D& start, // in: where the projectile starts
const Coord3D& end, // in: where the projectile wants to end up
Real velocity, // in: the initial speed of the projectile
Real minPitch, // in: min pitch (-PI/2)
Real maxPitch, // in: max pitch (-PI/2)
Bool preferShortPitch, // in: prefer the shorter or longer path?
Real& angle, // out: the angle to aim
Real& pitch // out: the pitch to aim for
)
{
Bool exactTarget = false;
angle = 0.0f;
pitch = 0.0f;
if (velocity <= 0.0f)
{
DEBUG_CRASH(("cant get there from here (1)"));
return false;
}
Real dx = end.x - start.x;
Real dy = end.y - start.y;
Real dz = end.z - start.z;
// calculating the angle is trivial.
angle = atan2(dy, dx);
// calculating the pitch requires a bit more effort.
Real horizDistSqr = sqr(dx) + sqr(dy);
Real horizDist = sqrt(horizDistSqr);
// calc the two possible pitches that will cover the given horizontal range.
// (this is actually only true if dz==0, but is a good first guess)
Real gravity = fabs(TheGlobalData->m_gravity);
Real gravityTwoDZ = gravity * 2.0f * dz;
// let's start by aiming directly for it. we know this isn't right (unless gravity
// is zero, which it's not) but is a good starting point...
Real theta = atan2(dz, horizDist);
// if the angle isn't pretty shallow, we can get a better initial guess by using
// the code below...
const Real SHALLOW_ANGLE = 0.5f * PI / 180.0f;
if (fabs(theta) > SHALLOW_ANGLE)
{
Real t = horizDist / velocity;
Real vz = (dz/t + 0.5f*gravity*t);
Real sineOfAngle = clamp(-1.0f, vz / velocity, 1.0f);
theta = ASin(sineOfAngle)*0.5f;
}
/*
this is, in theory, the "right" formula for dz==0, but I
get better results with the stuff above:
Real sineOfAngle = (gravity * horizDist) / sqr(velocity);
if (sineOfAngle > 1.0f)
{
return false;
}
Real theta = ASin(sineOfAngle)*0.5f;
*/
Real pitches[2];
Real cosPitches[2];
Real sinPitches[2];
Real theta_min = max(minPitch, -PI/2);
Real theta_max = min(maxPitch, PI/2);
const Real MIN_ANGLE_DIFF = (PI/(180.0f*16.0f)); // 1/16th of a degree. yes, we need that accuracy.
//Int numLoops = 0;
while (theta_max > theta_min + MIN_ANGLE_DIFF)
{
//++numLoops;
pitches[0] = theta; // shallower angle
pitches[1] = (theta >= 0.0) ? (PI/2 - theta) : (-PI/2 - theta); // steeper angle
DEBUG_ASSERTCRASH(pitches[0]<=PI/2&&pitches[0]>=-PI/2,("bad pitches[0] %f\n",rad2deg(pitches[0])));
DEBUG_ASSERTCRASH(pitches[1]<=PI/2&&pitches[1]>=-PI/2,("bad pitches[1] %f\n",rad2deg(pitches[1])));
// calc the horiz-speed & time for each.
// note that time can only be negative for 90=0&&t1>=0,("neg time"));
Int preferred = ((t0 < t1) == (preferShortPitch)) ? 0 : 1;
// ok, NOW... since dz is virtually NEVER zero, do a little approximation
// to get a better guess. (solving the equations directly are hard and
// it's simpler to approximate)
Bool tooClose = false;
Real vx, vz, root;
vz = velocity*sinPitches[preferred];
root = sqr(vz) - gravityTwoDZ;
if (root < 0.0f)
{
// oops, no solution for our preferred pitch. try the other one.
if (preferred == 0)
tooClose = true; // if this fails for the shallow case, it's 'cuz the result is too close
preferred = 1 - preferred;
vz = velocity*sinPitches[preferred];
root = sqr(vz) - gravityTwoDZ;
if (root < 0.0f)
{
DEBUG_CRASH(("cant get there from here (2)"));
return false;
}
}
#ifdef ONLY_RETURN_CLIPPED_PITCHES
// if we get this far, we have some plausible solution...
// it actually may be off, but let's go ahead and set the result
// (along with the "exact" flag) so that the caller will want to
// try to use the result. (we'll probably iterate and overwrite
// this result with a more precise one.)
if (within(minPitch, pitches[preferred], maxPitch))
{
pitch = pitches[preferred];
exactTarget = true;
}
#else
// if we get this far, we have some plausible solution...
// it actually may be off, but let's go ahead and set the result
// (along with the "exact" flag) so that the caller will want to
// try to use the result. (we'll probably iterate and overwrite
// this result with a more precise one.) NOTE however that we
// don't validate it's within the proper range... caller must do that!
pitch = pitches[preferred];
exactTarget = true;
#endif
vx = velocity*cosPitches[preferred];
Real actualRange = (vx*(vz + sqrt(root)))/gravity;
const Real CLOSE_ENOUGH_RANGE = 5.0f;
if (tooClose || (actualRange < horizDist - CLOSE_ENOUGH_RANGE))
{
theta_min = theta;
theta = (theta + theta_max) * 0.5f;
}
else if (actualRange > horizDist + CLOSE_ENOUGH_RANGE)
{
theta_max = theta;
theta = (theta + theta_min) * 0.5f;
}
else
{
break;
}
}
//DEBUG_LOG(("took %d loops to find a match\n",numLoops));
if (exactTarget)
return true;
return false;
}
#endif NOT_IN_USE
//-------------------------------------------------------------------------------------------------
// Prepares the missile for launch via proper weapon-system channels.
//-------------------------------------------------------------------------------------------------
void DumbProjectileBehavior::projectileLaunchAtObjectOrPosition(
const Object* victim,
const Coord3D* victimPos,
const Object* launcher,
WeaponSlotType wslot,
Int specificBarrelToUse,
const WeaponTemplate* detWeap,
const ParticleSystemTemplate* exhaustSysOverride
)
{
const DumbProjectileBehaviorModuleData* d = getDumbProjectileBehaviorModuleData();
DEBUG_ASSERTCRASH(specificBarrelToUse>=0, ("specificBarrelToUse must now be explicit"));
m_launcherID = launcher ? launcher->getID() : INVALID_ID;
m_extraBonusFlags = launcher ? launcher->getWeaponBonusCondition() : 0;
m_victimID = victim ? victim->getID() : INVALID_ID;
m_detonationWeaponTmpl = detWeap;
m_lifespanFrame = TheGameLogic->getFrame() + d->m_maxLifespan;
Object* projectile = getObject();
Weapon::positionProjectileForLaunch(projectile, launcher, wslot, specificBarrelToUse);
projectileFireAtObjectOrPosition( victim, victimPos, detWeap, exhaustSysOverride );
}
//-------------------------------------------------------------------------------------------------
// The actual firing of the missile once setup. Uses a Bezier curve with points parameterized in ini
//-------------------------------------------------------------------------------------------------
void DumbProjectileBehavior::projectileFireAtObjectOrPosition( const Object *victim, const Coord3D *victimPos, const WeaponTemplate *detWeap, const ParticleSystemTemplate* exhaustSysOverride )
{
const DumbProjectileBehaviorModuleData* d = getDumbProjectileBehaviorModuleData();
Object* projectile = getObject();
Real weaponSpeed = detWeap ? detWeap->getWeaponSpeed() : 0.0f;
Real minWeaponSpeed = detWeap ? detWeap->getMinWeaponSpeed() : 0.0f;
// if an object, aim at the center, not the ground part
Coord3D victimPosToUse;
if (victim)
victim->getGeometryInfo().getCenterPosition(*victim->getPosition(), victimPosToUse);
else
victimPosToUse = *victimPos;
if( detWeap && detWeap->isScaleWeaponSpeed() )
{
// Some weapons want to scale their start speed to the range
Real minRange = detWeap->getMinimumAttackRange();
Real maxRange = detWeap->getUnmodifiedAttackRange();
Real range = sqrt(ThePartitionManager->getDistanceSquared( projectile, &victimPosToUse, FROM_CENTER_2D ) );
Real rangeRatio = (range - minRange) / (maxRange - minRange);
m_flightPathSpeed = (rangeRatio * (weaponSpeed - minWeaponSpeed)) + minWeaponSpeed;
}
else
{
m_flightPathSpeed = weaponSpeed;
}
PhysicsBehavior* physics = projectile->getPhysics();
if ( d->m_tumbleRandomly && physics)
{
physics->setPitchRate( GameLogicRandomValueReal( -1.0f/PI, 1.0f/PI ) );
physics->setYawRate( GameLogicRandomValueReal( -1.0f/PI, 1.0f/PI ) );
physics->setRollRate( GameLogicRandomValueReal( -1.0f/PI, 1.0f/PI ) );
}
m_flightPathStart = *getObject()->getPosition();
m_flightPathEnd = victimPosToUse;
if (!calcFlightPath(true))
{
//Can only fail if wildly incorrect points
TheGameLogic->destroyObject( projectile );
return;
}
m_currentFlightPathStep = 0;// We are at the first point, because the launching put us there
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
Bool DumbProjectileBehavior::calcFlightPath(Bool recalcNumSegments)
{
const DumbProjectileBehaviorModuleData* d = getDumbProjectileBehaviorModuleData();
Coord3D controlPoints[4];
//First point is us, last point is them
controlPoints[0] = m_flightPathStart;
controlPoints[3] = m_flightPathEnd;
Real highestInterveningTerrain;
Bool onMap = ThePartitionManager->estimateTerrainExtremesAlongLine( controlPoints[0], controlPoints[3], NULL, &highestInterveningTerrain, NULL, NULL );
if( !onMap )
{
return false;
}
// X and Y for inner points are along the line between us, so normalize and scale a vector between us, but
// only use the x and y of the result
Vector3 targetVector;// 0 origin vector between me and him
targetVector.X = controlPoints[3].x - controlPoints[0].x;
targetVector.Y = controlPoints[3].y - controlPoints[0].y;
targetVector.Z = controlPoints[3].z - controlPoints[0].z;
Real targetDistance = targetVector.Length();
targetVector.Normalize();
Vector3 firstPointAlongLine = targetVector * (targetDistance * d->m_firstPercentIndent );
Vector3 secondPointAlongLine = targetVector * (targetDistance * d->m_secondPercentIndent );
controlPoints[1].x = firstPointAlongLine.X + controlPoints[0].x;// add world start to offset along the origin based vector
controlPoints[1].y = firstPointAlongLine.Y + controlPoints[0].y;
controlPoints[2].x = secondPointAlongLine.X + controlPoints[0].x;
controlPoints[2].y = secondPointAlongLine.Y + controlPoints[0].y;
// Z's are determined using the highest intervening height so they won't hit hills, low end bounded by current Zs
highestInterveningTerrain = max( highestInterveningTerrain, controlPoints[0].z );
highestInterveningTerrain = max( highestInterveningTerrain, controlPoints[3].z );
controlPoints[1].z = highestInterveningTerrain + d->m_firstHeight;
controlPoints[2].z = highestInterveningTerrain + d->m_secondHeight;
// With four control points, we have a curve. We will decide how many frames we want to take to get to the target,
// and fill our vector with those curve points.
BezierSegment flightCurve( controlPoints );
if (recalcNumSegments)
{
Real flightDistance = flightCurve.getApproximateLength();
m_flightPathSegments = ceil( flightDistance / m_flightPathSpeed );
}
flightCurve.getSegmentPoints( m_flightPathSegments, &m_flightPath );
DEBUG_ASSERTCRASH(m_flightPathSegments == m_flightPath.size(), ("m_flightPathSegments mismatch"));
#if defined(_DEBUG) || defined(_INTERNAL)
if( TheGlobalData->m_debugProjectilePath )
displayFlightPath();
#endif
return true;
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
Bool DumbProjectileBehavior::projectileHandleCollision( Object *other )
{
const DumbProjectileBehaviorModuleData* d = getDumbProjectileBehaviorModuleData();
if (other != NULL)
{
Object *projectileLauncher = TheGameLogic->findObjectByID( projectileGetLauncherID() );
// if it's not the specific thing we were targeting, see if we should incidentally collide...
if (!m_detonationWeaponTmpl->shouldProjectileCollideWith(projectileLauncher, getObject(), other, m_victimID))
{
//DEBUG_LOG(("ignoring projectile collision with %s at frame %d\n",other->getTemplate()->getName().str(),TheGameLogic->getFrame()));
return true;
}
if (d->m_garrisonHitKillCount > 0)
{
ContainModuleInterface* contain = other->getContain();
if( contain && contain->getContainCount() > 0 && contain->isGarrisonable() && !contain->isImmuneToClearBuildingAttacks() )
{
Int numKilled = 0;
// garrisonable buildings subvert the normal process here.
const ContainedItemsList* items = contain->getContainedItemsList();
if (items)
{
for (ContainedItemsList::const_iterator it = items->begin(); *it != NULL && numKilled < d->m_garrisonHitKillCount; )
{
Object* thingToKill = *it++;
if (!thingToKill->isEffectivelyDead() && thingToKill->isKindOfMulti(d->m_garrisonHitKillKindof, d->m_garrisonHitKillKindofNot))
{
//DEBUG_LOG(("Killed a garrisoned unit (%08lx %s) via Flash-Bang!\n",thingToKill,thingToKill->getTemplate()->getName().str()));
if (projectileLauncher)
projectileLauncher->scoreTheKill( thingToKill );
thingToKill->kill();
++numKilled;
}
} // next contained item
} // if items
if (numKilled > 0)
{
// note, fx is played at center of building, not at grenade's location
FXList::doFXObj(d->m_garrisonHitKillFX, other, NULL);
getObject()->getControllingPlayer()->getAcademyStats()->recordClearedGarrisonedBuilding();
// don't do the normal explosion; just destroy ourselves & return
TheGameLogic->destroyObject(getObject());
return true;
}
} // if a garrisonable thing
}
}
// collided with something... blow'd up!
detonate();
// mark ourself as "no collisions" (since we might still exist in slow death mode)
getObject()->setStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_NO_COLLISIONS ) );
return true;
}
//-------------------------------------------------------------------------------------------------
void DumbProjectileBehavior::detonate()
{
if ( m_hasDetonated )
return;
Object* obj = getObject();
if (m_detonationWeaponTmpl)
{
TheWeaponStore->handleProjectileDetonation(m_detonationWeaponTmpl, obj, obj->getPosition(), m_extraBonusFlags);
if ( getDumbProjectileBehaviorModuleData()->m_detonateCallsKill )
{
// don't call kill(); do it manually, so we can specify DEATH_DETONATED
DamageInfo damageInfo;
damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
damageInfo.in.m_deathType = DEATH_DETONATED;
damageInfo.in.m_sourceID = INVALID_ID;
damageInfo.in.m_amount = obj->getBodyModule()->getMaxHealth();
obj->attemptDamage( &damageInfo );
}
else
{
TheGameLogic->destroyObject( obj );
}
}
else
{
// don't call kill(); do it manually, so we can specify DEATH_DETONATED
DamageInfo damageInfo;
damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
damageInfo.in.m_deathType = DEATH_DETONATED;
damageInfo.in.m_sourceID = INVALID_ID;
damageInfo.in.m_amount = obj->getBodyModule()->getMaxHealth();
obj->attemptDamage( &damageInfo );
}
if (obj->getDrawable())
obj->getDrawable()->setDrawableHidden(true);
m_hasDetonated = TRUE;
}
//-------------------------------------------------------------------------------------------------
/**
* Simulate one frame of a missile's behavior
*/
UpdateSleepTime DumbProjectileBehavior::update()
{
const DumbProjectileBehaviorModuleData* d = getDumbProjectileBehaviorModuleData();
if (m_lifespanFrame != 0 && TheGameLogic->getFrame() >= m_lifespanFrame)
{
// lifetime demands detonation
detonate();
return UPDATE_SLEEP_NONE;
}
if( m_currentFlightPathStep >= m_flightPath.size() )
{
// No more steps to use. Would go out of bounds on vector, so have to do something.
// We could allow physics to take over and make us fall, but the point of this whole task
// is to guarentee where the shell explodes. This way, it _will_ explode at the target point.
detonate();
return UPDATE_SLEEP_NONE;
}
if (m_victimID != INVALID_ID && d->m_flightPathAdjustDistPerFrame > 0.0f)
{
Object* victim = TheGameLogic->findObjectByID(m_victimID);
if (victim)
{
Coord3D newVictimPos;
victim->getGeometryInfo().getCenterPosition(*victim->getPosition(), newVictimPos);
Coord3D delta;
delta.x = newVictimPos.x - m_flightPathEnd.x;
delta.y = newVictimPos.y - m_flightPathEnd.y;
delta.z = newVictimPos.z - m_flightPathEnd.z;
Real distVictimMovedSqr = sqr(delta.x) + sqr(delta.y) + sqr(delta.z);
if (distVictimMovedSqr > 0.1f)
{
Real distVictimMoved = sqrtf(distVictimMovedSqr);
if (distVictimMoved > d->m_flightPathAdjustDistPerFrame)
distVictimMoved = d->m_flightPathAdjustDistPerFrame;
delta.normalize();
m_flightPathEnd.x += distVictimMoved * delta.x;
m_flightPathEnd.y += distVictimMoved * delta.y;
m_flightPathEnd.z += distVictimMoved * delta.z;
if (!calcFlightPath(false))
{
DEBUG_CRASH(("Hmm, recalc of flight path returned false... should this happen?"));
detonate();
return UPDATE_SLEEP_NONE;
}
}
}
}
//Otherwise, continue to force the flight path
Coord3D flightStep = m_flightPath[m_currentFlightPathStep];
if (d->m_orientToFlightPath && (!d->m_tumbleRandomly) )
{
if ( m_currentFlightPathStep > 0)
{
// this seems reasonable; however, if this object has a PhysicsBehavior on it, this calc will be wrong,
// since Physics is applying gravity, which we duly ignore, but the prevPos won't be what we expect.
// get it from the flight path instead. (srj)
//Coord3D prevPos = *getObject()->getPosition();
Coord3D prevPos = m_flightPath[m_currentFlightPathStep - 1];
Vector3 curDir(flightStep.x - prevPos.x, flightStep.y - prevPos.y, flightStep.z - prevPos.z);
curDir.Normalize(); // buildTransformMatrix wants it this way
Matrix3D orientMtx;
orientMtx.buildTransformMatrix(Vector3(flightStep.x, flightStep.y, flightStep.z), curDir);
getObject()->setTransformMatrix(&orientMtx);
}
else // oops! how do we orient the projectile on the zeroeth frame? This didn't matter until we started using the
//long, blurry projectile graphics which look badly oriented on step 0 of the flight path
// so lets orient it the same as if it were on frame 1!
{
Coord3D prevPos = m_flightPath[0];
Coord3D curPos = m_flightPath[1];
Vector3 curDir(curPos.x - prevPos.x, curPos.y - prevPos.y, curPos.z - prevPos.z);
curDir.Normalize(); // buildTransformMatrix wants it this way
Matrix3D orientMtx;
orientMtx.buildTransformMatrix(Vector3(flightStep.x, flightStep.y, flightStep.z), curDir);
getObject()->setTransformMatrix(&orientMtx);
}
}
else
{
getObject()->setPosition(&flightStep);
}
// note that we want to use getHighestLayerForDestination() here, so that anything even slightly
// below the bridge translates into GROUND. (getLayerForDestination just does a "closest" check)
PathfindLayerEnum oldLayer = getObject()->getLayer();
PathfindLayerEnum newLayer = TheTerrainLogic->getHighestLayerForDestination(getObject()->getPosition());
getObject()->setLayer(newLayer);
if (oldLayer != LAYER_GROUND && newLayer == LAYER_GROUND)
{
// see if we' still in the bridge's xy area
Coord3D tmp = *getObject()->getPosition();
tmp.z = 9999.0f;
PathfindLayerEnum testLayer = TheTerrainLogic->getHighestLayerForDestination(&tmp);
if (testLayer == oldLayer)
{
// ensure we are slightly above the bridge, to account for fudge & sloppy art
const Real FUDGE = 2.0f;
tmp.z = TheTerrainLogic->getLayerHeight(tmp.x, tmp.y, testLayer) + FUDGE;
getObject()->setPosition(&tmp);
// blow'd up!
detonate();
return UPDATE_SLEEP_NONE;
}
}
++m_currentFlightPathStep;
return UPDATE_SLEEP_NONE;//This no longer flys with physics, so it needs to not sleep
}
// ------------------------------------------------------------------------------------------------
/** displayFlightPath for debugging */
// ------------------------------------------------------------------------------------------------
#if defined(_DEBUG) || defined(_INTERNAL)
void DumbProjectileBehavior::displayFlightPath()
{
extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color);
for( Int pointIndex = 0; pointIndex < m_flightPath.size(); ++pointIndex )
{
addIcon(&m_flightPath[pointIndex], TheGlobalData->m_debugProjectileTileWidth,
TheGlobalData->m_debugProjectileTileDuration,
TheGlobalData->m_debugProjectileTileColor);
}
}
#endif
// ------------------------------------------------------------------------------------------------
/** CRC */
// ------------------------------------------------------------------------------------------------
void DumbProjectileBehavior::crc( Xfer *xfer )
{
// extend base class
UpdateModule::crc( xfer );
} // end crc
// ------------------------------------------------------------------------------------------------
/** Xfer method
* Version Info:
* 1: Initial version */
// ------------------------------------------------------------------------------------------------
void DumbProjectileBehavior::xfer( Xfer *xfer )
{
// version
XferVersion currentVersion = 1;
XferVersion version = currentVersion;
xfer->xferVersion( &version, currentVersion );
// extend base class
UpdateModule::xfer( xfer );
// launcher
xfer->xferObjectID( &m_launcherID );
// victim ID
xfer->xferObjectID( &m_victimID );
xfer->xferInt( &m_flightPathSegments );
xfer->xferReal( &m_flightPathSpeed );
xfer->xferCoord3D( &m_flightPathStart );
xfer->xferCoord3D( &m_flightPathEnd );
// weapon template
AsciiString weaponTemplateName = AsciiString::TheEmptyString;
if( m_detonationWeaponTmpl )
weaponTemplateName = m_detonationWeaponTmpl->getName();
xfer->xferAsciiString( &weaponTemplateName );
if( xfer->getXferMode() == XFER_LOAD )
{
if( weaponTemplateName == AsciiString::TheEmptyString )
m_detonationWeaponTmpl = NULL;
else
{
// find template
m_detonationWeaponTmpl = TheWeaponStore->findWeaponTemplate( weaponTemplateName );
// sanity
if( m_detonationWeaponTmpl == NULL )
{
DEBUG_CRASH(( "DumbProjectileBehavior::xfer - Unknown weapon template '%s'\n",
weaponTemplateName.str() ));
throw SC_INVALID_DATA;
} // end if
} // end else
} // end if
// lifespan frame
xfer->xferUnsignedInt( &m_lifespanFrame );
} // end xfer
// ------------------------------------------------------------------------------------------------
/** Load post process */
// ------------------------------------------------------------------------------------------------
void DumbProjectileBehavior::loadPostProcess( void )
{
// extend base class
UpdateModule::loadPostProcess();
} // end loadPostProcess