2251 lines
62 KiB
C
2251 lines
62 KiB
C
/*
|
|
This file is part of Warzone 2100.
|
|
Copyright (C) 1999-2004 Eidos Interactive
|
|
Copyright (C) 2005-2007 Warzone Resurrection Project
|
|
|
|
Warzone 2100 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 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Warzone 2100 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 Warzone 2100; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
*/
|
|
/***************************************************************************/
|
|
/*
|
|
* Projectile functions
|
|
*
|
|
*/
|
|
/***************************************************************************/
|
|
#include <string.h>
|
|
|
|
#include "lib/framework/frame.h"
|
|
#include "lib/framework/trig.h"
|
|
#include "lib/framework/math-help.h"
|
|
|
|
#include "lib/gamelib/gtime.h"
|
|
#include "objects.h"
|
|
#include "move.h"
|
|
#include "action.h"
|
|
#include "combat.h"
|
|
#include "effects.h"
|
|
#include "map.h"
|
|
#include "lib/sound/audio_id.h"
|
|
#include "lib/sound/audio.h"
|
|
#include "anim_id.h"
|
|
#include "projectile.h"
|
|
#include "visibility.h"
|
|
#include "lib/script/script.h"
|
|
#include "scripttabs.h"
|
|
#include "scriptcb.h"
|
|
#include "group.h"
|
|
#include "cmddroid.h"
|
|
#include "feature.h"
|
|
#include "lib/ivis_common/piestate.h"
|
|
#include "loop.h"
|
|
// FIXME Direct iVis implementation include!
|
|
#include "lib/ivis_opengl/piematrix.h"
|
|
|
|
#include "scores.h"
|
|
|
|
#include "display3d.h"
|
|
#include "display.h"
|
|
#include "multiplay.h"
|
|
#include "multistat.h"
|
|
#include "mapgrid.h"
|
|
|
|
#define PROJ_MAX_PITCH 30
|
|
#define ACC_GRAVITY 1000
|
|
#define DIRECT_PROJ_SPEED 500
|
|
#define NOMINAL_DAMAGE 5
|
|
#define VTOL_HITBOX_MODIFICATOR 100
|
|
|
|
/** Used for passing data to the checkBurnDamage function */
|
|
typedef struct
|
|
{
|
|
SWORD x1, y1;
|
|
SWORD x2, y2;
|
|
SWORD rad;
|
|
} FIRE_BOX;
|
|
|
|
// Watermelon:they are from droid.c
|
|
/* The range for neighbouring objects */
|
|
#define PROJ_NAYBOR_RANGE (TILE_UNITS*4)
|
|
|
|
// Watermelon:neighbour global info ripped from droid.c
|
|
static PROJ_NAYBOR_INFO asProjNaybors[MAX_NAYBORS];
|
|
static UDWORD numProjNaybors = 0;
|
|
|
|
static BASE_OBJECT *CurrentProjNaybors = NULL;
|
|
static UDWORD projnayborTime = 0;
|
|
|
|
/* The list of projectiles in play */
|
|
static PROJECTILE *psProjectileList = NULL;
|
|
|
|
/* The next projectile to give out in the proj_First / proj_Next methods */
|
|
static PROJECTILE *psProjectileNext = NULL;
|
|
|
|
/***************************************************************************/
|
|
|
|
// the last unit that did damage - used by script functions
|
|
BASE_OBJECT *g_pProjLastAttacker;
|
|
|
|
extern BOOL godMode;
|
|
|
|
/***************************************************************************/
|
|
|
|
static UDWORD establishTargetRadius( BASE_OBJECT *psTarget );
|
|
static UDWORD establishTargetHeight( BASE_OBJECT *psTarget );
|
|
static void proj_InFlightDirectFunc( PROJECTILE *psObj );
|
|
static void proj_InFlightIndirectFunc( PROJECTILE *psObj );
|
|
static void proj_ImpactFunc( PROJECTILE *psObj );
|
|
static void proj_PostImpactFunc( PROJECTILE *psObj );
|
|
static void proj_checkBurnDamage( BASE_OBJECT *apsList, PROJECTILE *psProj,
|
|
FIRE_BOX *pFireBox );
|
|
static void proj_Free(PROJECTILE *psObj);
|
|
|
|
static float objectDamage(BASE_OBJECT *psObj, UDWORD damage, UDWORD weaponClass,UDWORD weaponSubClass, HIT_SIDE impactSide);
|
|
static HIT_SIDE getHitSide (PROJECTILE *psObj, BASE_OBJECT *psTarget);
|
|
|
|
static void projGetNaybors(PROJECTILE *psObj);
|
|
|
|
|
|
/***************************************************************************/
|
|
BOOL gfxVisible(PROJECTILE *psObj)
|
|
{
|
|
BOOL bVisible = false;
|
|
|
|
// Already know it is visible
|
|
if (psObj->bVisible)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// You fired it
|
|
if (psObj->player == selectedPlayer)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Always see in this mode
|
|
if (godMode)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// You can see the source
|
|
if (psObj->psSource != NULL
|
|
&& !psObj->psSource->died
|
|
&& psObj->psSource->visible[selectedPlayer])
|
|
{
|
|
bVisible = true;
|
|
}
|
|
|
|
// You can see the destination
|
|
if (psObj->psDest != NULL
|
|
&& !psObj->psDest->died
|
|
&& psObj->psDest->visible[selectedPlayer])
|
|
{
|
|
bVisible = true;
|
|
}
|
|
|
|
// Someone elses structure firing at something you can't see
|
|
if (psObj->psSource != NULL
|
|
&& !psObj->psSource->died
|
|
&& psObj->psSource->type == OBJ_STRUCTURE
|
|
&& psObj->psSource->player != selectedPlayer
|
|
&& (psObj->psDest == NULL
|
|
|| psObj->psDest->died
|
|
|| !psObj->psDest->visible[selectedPlayer]))
|
|
{
|
|
bVisible = false;
|
|
}
|
|
|
|
// Something you cannot see firing at a structure that isn't yours
|
|
if (psObj->psDest != NULL
|
|
&& !psObj->psDest->died
|
|
&& psObj->psDest->type == OBJ_STRUCTURE
|
|
&& psObj->psDest->player != selectedPlayer
|
|
&& (psObj->psSource == NULL
|
|
|| !psObj->psSource->visible[selectedPlayer]))
|
|
{
|
|
bVisible = false;
|
|
}
|
|
|
|
return bVisible;
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
BOOL
|
|
proj_InitSystem( void )
|
|
{
|
|
psProjectileList = NULL;
|
|
psProjectileNext = NULL;
|
|
|
|
return true;
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
// Clean out all projectiles from the system, and properly decrement
|
|
// all reference counts.
|
|
void
|
|
proj_FreeAllProjectiles( void )
|
|
{
|
|
PROJECTILE *psCurr = psProjectileList, *psPrev = NULL;
|
|
|
|
while (psCurr)
|
|
{
|
|
psPrev = psCurr;
|
|
psCurr = psCurr->psNext;
|
|
proj_Free(psPrev);
|
|
}
|
|
|
|
psProjectileList = NULL;
|
|
psProjectileNext = NULL;
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
BOOL
|
|
proj_Shutdown( void )
|
|
{
|
|
proj_FreeAllProjectiles();
|
|
|
|
return true;
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
// Free the memory held by a projectile, and decrement its reference counts,
|
|
// if any. Do not call directly on a projectile in a list, because then the
|
|
// list will be broken!
|
|
static void proj_Free(PROJECTILE *psObj)
|
|
{
|
|
/* Decrement any reference counts the projectile may have increased */
|
|
setProjectileDamaged(psObj, NULL);
|
|
setProjectileSource(psObj, NULL);
|
|
setProjectileDestination(psObj, NULL);
|
|
|
|
free(psObj);
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
// Reset the first/next methods, and give out the first projectile in the list.
|
|
PROJECTILE *
|
|
proj_GetFirst( void )
|
|
{
|
|
psProjectileNext = psProjectileList;
|
|
return psProjectileList;
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
// Get the next projectile
|
|
PROJECTILE *
|
|
proj_GetNext( void )
|
|
{
|
|
psProjectileNext = psProjectileNext->psNext;
|
|
return psProjectileNext;
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
/*
|
|
* Relates the quality of the attacker to the quality of the victim.
|
|
* The value returned satisfies the following inequality: 0.5 <= ret <= 2.0
|
|
*/
|
|
static float QualityFactor(DROID *psAttacker, DROID *psVictim)
|
|
{
|
|
float powerRatio = calcDroidPower(psVictim) / calcDroidPower(psAttacker);
|
|
float pointsRatio = calcDroidPoints(psVictim) / calcDroidPoints(psAttacker);
|
|
|
|
CLIP(powerRatio, 0.5, 2.0);
|
|
CLIP(pointsRatio, 0.5, 2.0);
|
|
|
|
return (powerRatio + pointsRatio) / 2;
|
|
}
|
|
|
|
// update the kills after a target is damaged/destroyed
|
|
static void proj_UpdateKills(PROJECTILE *psObj, float experienceInc)
|
|
{
|
|
DROID *psDroid;
|
|
BASE_OBJECT *psSensor;
|
|
|
|
CHECK_PROJECTILE(psObj);
|
|
|
|
if ((psObj->psSource == NULL) ||
|
|
((psObj->psDest != NULL) && (psObj->psDest->type == OBJ_FEATURE)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If experienceInc is negative then the target was killed
|
|
if (bMultiPlayer && experienceInc < 0.0f)
|
|
{
|
|
updateMultiStatsKills(psObj->psDest, psObj->psSource->player);
|
|
}
|
|
|
|
// Since we are no longer interested if it was killed or not, abs it
|
|
experienceInc = fabs(experienceInc);
|
|
|
|
if (psObj->psSource->type == OBJ_DROID) /* update droid kills */
|
|
{
|
|
psDroid = (DROID *) psObj->psSource;
|
|
|
|
// If it is 'droid-on-droid' then modify the experience by the Quality factor
|
|
// Only do this in MP so to not un-balance the campaign
|
|
if (psObj->psDest != NULL
|
|
&& psObj->psDest->type == OBJ_DROID
|
|
&& bMultiPlayer)
|
|
{
|
|
// Modify the experience gained by the 'quality factor' of the units
|
|
experienceInc *= QualityFactor(psDroid, (DROID *) psObj->psDest);
|
|
}
|
|
|
|
psDroid->experience += experienceInc;
|
|
cmdDroidUpdateKills(psDroid, experienceInc);
|
|
|
|
psSensor = orderStateObj(psDroid, DORDER_FIRESUPPORT);
|
|
if (psSensor
|
|
&& psSensor->type == OBJ_DROID)
|
|
{
|
|
((DROID *) psSensor)->experience += experienceInc;
|
|
}
|
|
}
|
|
else if (psObj->psSource->type == OBJ_STRUCTURE)
|
|
{
|
|
// See if there was a command droid designating this target
|
|
psDroid = cmdDroidGetDesignator(psObj->psSource->player);
|
|
|
|
if (psDroid != NULL
|
|
&& psDroid->action == DACTION_ATTACK
|
|
&& psDroid->psActionTarget[0] == psObj->psDest)
|
|
{
|
|
psDroid->experience += experienceInc;
|
|
}
|
|
}
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
BOOL proj_SendProjectile(WEAPON *psWeap, BASE_OBJECT *psAttacker, int player, Vector3i target, BASE_OBJECT *psTarget, BOOL bVisible, int weapon_slot)
|
|
{
|
|
PROJECTILE *psObj = malloc(sizeof(PROJECTILE));
|
|
SDWORD tarHeight, srcHeight, iMinSq;
|
|
SDWORD altChange, dx, dy, dz, iVelSq, iVel;
|
|
double fR, fA, fS, fT, fC;
|
|
Vector3f muzzle;
|
|
SDWORD iRadSq, iPitchLow, iPitchHigh, iTemp;
|
|
UDWORD heightVariance;
|
|
WEAPON_STATS *psStats = &asWeaponStats[psWeap->nStat];
|
|
|
|
ASSERT( psStats != NULL, "proj_SendProjectile: invalid weapon stats" );
|
|
|
|
ASSERT( psTarget == NULL || !psTarget->died, "Aiming at dead target!" );
|
|
|
|
/* get muzzle offset */
|
|
if (psAttacker == NULL)
|
|
{
|
|
// if there isn't an attacker just start at the target position
|
|
// NB this is for the script function to fire the las sats
|
|
muzzle = Vector3f_New(target.x, target.y, target.z);
|
|
}
|
|
else if (psAttacker->type == OBJ_DROID && weapon_slot >= 0)
|
|
{
|
|
calcDroidMuzzleLocation( (DROID *) psAttacker, &muzzle, weapon_slot);
|
|
/*update attack runs for VTOL droid's each time a shot is fired*/
|
|
updateVtolAttackRun((DROID *)psAttacker, weapon_slot);
|
|
}
|
|
else if (psAttacker->type == OBJ_STRUCTURE && weapon_slot >= 0)
|
|
{
|
|
calcStructureMuzzleLocation( (STRUCTURE *) psAttacker, &muzzle, weapon_slot);
|
|
}
|
|
else // incase anything wants a projectile
|
|
{
|
|
muzzle = Vector3f_New(psAttacker->pos.x, psAttacker->pos.y, psAttacker->pos.z);
|
|
}
|
|
|
|
/* Initialise the structure */
|
|
psObj->type = OBJ_PROJECTILE;
|
|
psObj->state = PROJ_INFLIGHT;
|
|
psObj->psWStats = psStats;
|
|
psObj->pos = Vector3uw_New(muzzle.x, muzzle.y, muzzle.z);
|
|
psObj->startX = muzzle.x;
|
|
psObj->startY = muzzle.y;
|
|
psObj->tarX = target.x;
|
|
psObj->tarY = target.y;
|
|
psObj->targetRadius = (psTarget ? establishTargetRadius(psTarget) : 0); // needed to backtrack FX
|
|
psObj->born = gameTime;
|
|
psObj->player = (UBYTE)player;
|
|
psObj->bVisible = false;
|
|
psObj->airTarget = false;
|
|
psObj->psDamaged = NULL; // must initialize these to NULL first!
|
|
psObj->psSource = NULL;
|
|
psObj->psDest = NULL;
|
|
psObj->died = 0;
|
|
setProjectileDestination(psObj, psTarget);
|
|
|
|
/* If target is a VTOL or higher than ground, it is an air target. */
|
|
if ((psTarget != NULL && psTarget->type == OBJ_DROID && isVtolDroid((DROID*)psTarget))
|
|
|| (psTarget == NULL && target.z > map_Height(target.x, target.y)))
|
|
{
|
|
psObj->airTarget = true;
|
|
}
|
|
|
|
//Watermelon:use the source of the source of psObj :) (psAttacker is a projectile)
|
|
if (psAttacker && psAttacker->type == OBJ_PROJECTILE)
|
|
{
|
|
// psAttacker is a projectile if bPenetrate
|
|
PROJECTILE *psProj = (PROJECTILE*)psAttacker;
|
|
|
|
if (psProj->psSource && !psProj->psSource->died)
|
|
{
|
|
setProjectileSource(psObj, psProj->psSource);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
setProjectileSource(psObj, psAttacker);
|
|
}
|
|
|
|
if (psTarget)
|
|
{
|
|
scoreUpdateVar(WD_SHOTS_ON_TARGET);
|
|
heightVariance = 0;
|
|
switch(psTarget->type)
|
|
{
|
|
case OBJ_DROID:
|
|
case OBJ_FEATURE:
|
|
if( ((DROID*)psTarget)->droidType == DROID_PERSON )
|
|
{
|
|
heightVariance = rand()%4;
|
|
}
|
|
else
|
|
{
|
|
heightVariance = rand()%8;
|
|
}
|
|
break;
|
|
|
|
case OBJ_STRUCTURE:
|
|
heightVariance = rand()%8;
|
|
break;
|
|
|
|
case OBJ_PROJECTILE:
|
|
ASSERT(!"invalid object type: bullet", "proj_SendProjectile: invalid object type: OBJ_PROJECTILE");
|
|
break;
|
|
|
|
case OBJ_TARGET:
|
|
ASSERT(!"invalid object type: target", "proj_SendProjectile: invalid object type: OBJ_TARGET");
|
|
break;
|
|
|
|
default:
|
|
ASSERT(!"unknown object type", "proj_SendProjectile: unknown object type");
|
|
break;
|
|
}
|
|
tarHeight = psTarget->pos.z + heightVariance;
|
|
}
|
|
else
|
|
{
|
|
tarHeight = target.z;
|
|
scoreUpdateVar(WD_SHOTS_OFF_TARGET);
|
|
}
|
|
|
|
srcHeight = muzzle.z;
|
|
altChange = tarHeight - srcHeight;
|
|
|
|
psObj->srcHeight = srcHeight;
|
|
psObj->altChange = altChange;
|
|
|
|
dx = ((SDWORD)psObj->tarX) - muzzle.x;
|
|
dy = ((SDWORD)psObj->tarY) - muzzle.y;
|
|
dz = tarHeight - muzzle.z;
|
|
|
|
/* roll never set */
|
|
psObj->roll = 0;
|
|
|
|
fR = atan2(dx, dy);
|
|
if ( fR < 0.0 )
|
|
{
|
|
fR += 2.0 * M_PI;
|
|
}
|
|
psObj->direction = RAD_TO_DEG(fR);
|
|
|
|
|
|
/* get target distance */
|
|
iRadSq = dx*dx + dy*dy + dz*dz;
|
|
fR = trigIntSqrt( iRadSq );
|
|
iMinSq = psStats->minRange * psStats->minRange;
|
|
|
|
if ( proj_Direct(psStats) ||
|
|
( !proj_Direct(psStats) && (iRadSq <= iMinSq) ) )
|
|
{
|
|
fR = atan2(dz, fR);
|
|
if ( fR < 0.0 )
|
|
{
|
|
fR += 2.0 * M_PI;
|
|
}
|
|
psObj->pitch = (SWORD)( RAD_TO_DEG(fR) );
|
|
psObj->pInFlightFunc = proj_InFlightDirectFunc;
|
|
}
|
|
else
|
|
{
|
|
/* indirect */
|
|
iVelSq = psStats->flightSpeed * psStats->flightSpeed;
|
|
|
|
fA = ACC_GRAVITY * (double)iRadSq / (2.0 * iVelSq);
|
|
fC = 4.0 * fA * (dz + fA);
|
|
fS = (double)iRadSq - fC;
|
|
|
|
/* target out of range - increase velocity to hit target */
|
|
if ( fS < 0.0 )
|
|
{
|
|
/* set optimal pitch */
|
|
psObj->pitch = PROJ_MAX_PITCH;
|
|
|
|
fS = trigSin(PROJ_MAX_PITCH);
|
|
fC = trigCos(PROJ_MAX_PITCH);
|
|
fT = fS / fC;
|
|
fS = ACC_GRAVITY * (1. + fT * fT);
|
|
fS = fS / (2.0 * (fR * fT - dz));
|
|
{
|
|
iVel = trigIntSqrt(fS * (fR * fR));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* set velocity to stats value */
|
|
iVel = psStats->flightSpeed;
|
|
|
|
/* get floating point square root */
|
|
fS = trigIntSqrt(fS);
|
|
|
|
fT = atan2(fR + fS, 2.0 * fA);
|
|
|
|
/* make sure angle positive */
|
|
if ( fT < 0.0 )
|
|
{
|
|
fT += 2.0 * M_PI;
|
|
}
|
|
iPitchLow = RAD_TO_DEG(fT);
|
|
|
|
fT = atan2(fR-fS, 2.0*fA);
|
|
/* make sure angle positive */
|
|
if ( fT < 0.0 )
|
|
{
|
|
fT += 2.0 * M_PI;
|
|
}
|
|
iPitchHigh = RAD_TO_DEG(fT);
|
|
|
|
/* swap pitches if wrong way round */
|
|
if ( iPitchLow > iPitchHigh )
|
|
{
|
|
iTemp = iPitchHigh;
|
|
iPitchLow = iPitchHigh;
|
|
iPitchHigh = iTemp;
|
|
}
|
|
|
|
/* chooselow pitch unless -ve */
|
|
if ( iPitchLow > 0 )
|
|
{
|
|
psObj->pitch = (SWORD)iPitchLow;
|
|
}
|
|
else
|
|
{
|
|
psObj->pitch = (SWORD)iPitchHigh;
|
|
}
|
|
}
|
|
|
|
/* if droid set muzzle pitch */
|
|
//Watermelon:fix turret pitch for more turrets
|
|
if (psAttacker != NULL && weapon_slot >= 0)
|
|
{
|
|
if (psAttacker->type == OBJ_DROID)
|
|
{
|
|
((DROID *) psAttacker)->turretPitch[weapon_slot] = psObj->pitch;
|
|
}
|
|
else if (psAttacker->type == OBJ_STRUCTURE)
|
|
{
|
|
((STRUCTURE *) psAttacker)->turretPitch[weapon_slot] = psObj->pitch;
|
|
}
|
|
}
|
|
|
|
psObj->vXY = iVel * trigCos(psObj->pitch);
|
|
psObj->vZ = iVel * trigSin(psObj->pitch);
|
|
|
|
/* set function pointer */
|
|
psObj->pInFlightFunc = proj_InFlightIndirectFunc;
|
|
}
|
|
|
|
/* put the projectile object first in the global list */
|
|
psObj->psNext = psProjectileList;
|
|
psProjectileList = psObj;
|
|
|
|
/* play firing audio */
|
|
// only play if either object is visible, i know it's a bit of a hack, but it avoids the problem
|
|
// of having to calculate real visibility values for each projectile.
|
|
if ( bVisible || gfxVisible(psObj) )
|
|
{
|
|
// note that the projectile is visible
|
|
psObj->bVisible = true;
|
|
|
|
if ( psStats->iAudioFireID != NO_SOUND )
|
|
{
|
|
|
|
if ( psObj->psSource )
|
|
{
|
|
/* firing sound emitted from source */
|
|
audio_PlayObjDynamicTrack( (BASE_OBJECT *) psObj->psSource,
|
|
psStats->iAudioFireID, NULL );
|
|
/* GJ HACK: move howitzer sound with shell */
|
|
if ( psStats->weaponSubClass == WSC_HOWITZERS )
|
|
{
|
|
audio_PlayObjDynamicTrack( (BASE_OBJECT *) psObj,
|
|
ID_SOUND_HOWITZ_FLIGHT, NULL );
|
|
}
|
|
}
|
|
//don't play the sound for a LasSat in multiPlayer
|
|
else if (!(bMultiPlayer && psStats->weaponSubClass == WSC_LAS_SAT))
|
|
{
|
|
audio_PlayObjStaticTrack(psObj, psStats->iAudioFireID);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((psAttacker != NULL) && !proj_Direct(psStats))
|
|
{
|
|
//check for Counter Battery Sensor in range of target
|
|
counterBatteryFire(psAttacker, psTarget);
|
|
}
|
|
|
|
CHECK_PROJECTILE(psObj);
|
|
|
|
return true;
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
static void proj_InFlightDirectFunc(PROJECTILE *psProj)
|
|
{
|
|
/* we want a delay between Las-Sats firing and actually hitting in multiPlayer
|
|
magic number but that's how long the audio countdown message lasts! */
|
|
const unsigned int LAS_SAT_DELAY = 8;
|
|
int timeSoFar;
|
|
int distancePercent; /* How far we are 0..100 */
|
|
float distanceRatio; /* How far we are, 1.0==at target */
|
|
float distanceExtensionFactor; /* Extended lifespan */
|
|
Vector3i move;
|
|
unsigned int i;
|
|
// Projectile is missile:
|
|
bool bMissile = false;
|
|
WEAPON_STATS *psStats;
|
|
|
|
CHECK_PROJECTILE(psProj);
|
|
|
|
timeSoFar = gameTime - psProj->born;
|
|
|
|
psStats = psProj->psWStats;
|
|
ASSERT(psStats != NULL, "proj_InFlightDirectFunc: Invalid weapon stats pointer");
|
|
|
|
/* we want a delay between Las-Sats firing and actually hitting in multiPlayer
|
|
magic number but that's how long the audio countdown message lasts! */
|
|
if (bMultiPlayer && psStats->weaponSubClass == WSC_LAS_SAT &&
|
|
timeSoFar < LAS_SAT_DELAY * GAME_TICKS_PER_SEC)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* Calculate extended lifespan where appropriate */
|
|
switch (psStats->weaponSubClass)
|
|
{
|
|
case WSC_MGUN:
|
|
case WSC_COMMAND:
|
|
distanceExtensionFactor = 1.2f;
|
|
break;
|
|
case WSC_CANNON:
|
|
case WSC_BOMB:
|
|
case WSC_ELECTRONIC:
|
|
case WSC_EMP:
|
|
case WSC_FLAME:
|
|
case WSC_ENERGY:
|
|
case WSC_GAUSS:
|
|
distanceExtensionFactor = 1.5f;
|
|
break;
|
|
case WSC_AAGUN: // No extended distance
|
|
distanceExtensionFactor = 1.0f;
|
|
break;
|
|
case WSC_ROCKET:
|
|
case WSC_MISSILE:
|
|
case WSC_SLOWROCKET:
|
|
case WSC_SLOWMISSILE:
|
|
bMissile = true; // Take the same extended targetDistance as artillery
|
|
case WSC_COUNTER:
|
|
case WSC_MORTARS:
|
|
case WSC_HOWITZERS:
|
|
case WSC_LAS_SAT:
|
|
distanceExtensionFactor = 1.5f;
|
|
break;
|
|
default:
|
|
// WSC_NUM_WEAPON_SUBCLASSES
|
|
/* Uninitialized "marker", this can be used as a
|
|
* condition to assert on (i.e. it shouldn't occur).
|
|
*/
|
|
distanceExtensionFactor = 0.f;
|
|
break;
|
|
}
|
|
|
|
/* Do movement */
|
|
{
|
|
unsigned int targetDistance, currentDistance;
|
|
|
|
/* Calculate movement vector: */
|
|
if (psStats->movementModel == MM_HOMINGDIRECT && psProj->psDest)
|
|
{
|
|
/* If it's homing and it has a target (not a miss)... */
|
|
move.x = psProj->psDest->pos.x - psProj->startX;
|
|
move.y = psProj->psDest->pos.y - psProj->startY;
|
|
move.z = psProj->psDest->pos.z - psProj->srcHeight;
|
|
}
|
|
else
|
|
{
|
|
move.x = psProj->tarX - psProj->startX;
|
|
move.y = psProj->tarY - psProj->startY;
|
|
move.z = psProj->altChange;
|
|
}
|
|
|
|
targetDistance = sqrtf(move.x*move.x + move.y*move.y);
|
|
currentDistance = timeSoFar * psStats->flightSpeed / GAME_TICKS_PER_SEC;
|
|
|
|
// Prevent div by zero:
|
|
if (targetDistance == 0)
|
|
targetDistance = 1;
|
|
|
|
distanceRatio = (float)currentDistance / targetDistance;
|
|
distancePercent = PERCENT(currentDistance, targetDistance);
|
|
|
|
{
|
|
/* Calculate next position */
|
|
Vector3uw nextPos = {
|
|
psProj->startX + (distanceRatio * move.x),
|
|
psProj->startY + (distanceRatio * move.y),
|
|
psProj->srcHeight + (distanceRatio * move.z)
|
|
};
|
|
|
|
/* impact if about to go off map else update coordinates */
|
|
if (!worldOnMap(nextPos.x, nextPos.y))
|
|
{
|
|
psProj->state = PROJ_IMPACT;
|
|
setProjectileDestination(psProj, NULL);
|
|
debug(LOG_NEVER, "**** projectile(%i) off map - removed ****\n", psProj->id);
|
|
return;
|
|
}
|
|
|
|
/* Update position */
|
|
psProj->pos = nextPos;
|
|
}
|
|
}
|
|
|
|
/* Check nearby objects for possible collisions */
|
|
for (i = 0; i < numProjNaybors; i++)
|
|
{
|
|
BASE_OBJECT *psTempObj = asProjNaybors[i].psObj;
|
|
|
|
CHECK_OBJECT(psTempObj);
|
|
|
|
if (psTempObj == psProj->psDamaged)
|
|
{
|
|
// Dont damage one target twice
|
|
continue;
|
|
}
|
|
|
|
if (psTempObj->died)
|
|
{
|
|
// Do not damage dead objects further
|
|
continue;
|
|
}
|
|
|
|
if (psTempObj->type == OBJ_PROJECTILE &&
|
|
!(bMissile || ((PROJECTILE*)psTempObj)->psWStats->weaponSubClass == WSC_COUNTER))
|
|
{
|
|
// A projectile should not collide with another projectile unless it's a counter-missile weapon
|
|
continue;
|
|
}
|
|
|
|
if (psTempObj->type == OBJ_FEATURE &&
|
|
!((FEATURE*)psTempObj)->psStats->damageable)
|
|
{
|
|
// Ignore oil resources, artifacts and other pickups
|
|
continue;
|
|
}
|
|
|
|
if (psTempObj->player == psProj->player ||
|
|
aiCheckAlliances(psTempObj->player, psProj->player))
|
|
{
|
|
// No friendly fire
|
|
continue;
|
|
}
|
|
|
|
if (psStats->surfaceToAir == SHOOT_IN_AIR &&
|
|
(psTempObj->type == OBJ_STRUCTURE ||
|
|
psTempObj->type == OBJ_FEATURE ||
|
|
(psTempObj->type == OBJ_DROID && !isVtolDroid((DROID *)psTempObj))
|
|
))
|
|
{
|
|
// AA weapons should not hit buildings and non-vtol droids
|
|
continue;
|
|
}
|
|
|
|
/* Actual collision test */
|
|
{
|
|
// FIXME HACK Needed since we got those ugly Vector3uw floating around in BASE_OBJECT...
|
|
Vector3i
|
|
posProj = {psProj->pos.x, psProj->pos.y, psProj->pos.z},
|
|
posTemp = {psTempObj->pos.x, psTempObj->pos.y, psTempObj->pos.z};
|
|
|
|
Vector3i diff = Vector3i_Sub(posProj, posTemp);
|
|
|
|
unsigned int targetHeight = establishTargetHeight(psTempObj);
|
|
unsigned int targetRadius = establishTargetRadius(psTempObj);
|
|
|
|
/* Height is always positive */
|
|
diff.z = abs(diff.z);
|
|
|
|
/* We hit! */
|
|
if (diff.z < targetHeight &&
|
|
(diff.x*diff.x + diff.y*diff.y) < targetRadius * targetRadius)
|
|
{
|
|
setProjectileDestination(psProj, psTempObj);
|
|
|
|
/* Buildings cannot be penetrated and we need a penetrating weapon */
|
|
if (psTempObj->type == OBJ_DROID && psStats->penetrate)
|
|
{
|
|
WEAPON asWeap = {psStats - asWeaponStats, 0, 0, 0};
|
|
// Determine position to fire a missile at
|
|
// (must be at least 0 because we don't use signed integers
|
|
// this shouldn't be larger than the height and width of the map either)
|
|
Vector3i newDest = {
|
|
psProj->startX + move.x * distanceExtensionFactor,
|
|
psProj->startY + move.y * distanceExtensionFactor,
|
|
psProj->srcHeight + move.z * distanceExtensionFactor
|
|
};
|
|
|
|
ASSERT(distanceExtensionFactor != 0.f, "Unitialized variable used! distanceExtensionFactor is not initialized.");
|
|
|
|
newDest.x = clip(newDest.x, 0, world_coord(mapWidth - 1));
|
|
newDest.y = clip(newDest.y, 0, world_coord(mapHeight - 1));
|
|
|
|
// Assume we damaged the chosen target
|
|
setProjectileDamaged(psProj, psTempObj);
|
|
|
|
proj_SendProjectile(&asWeap, (BASE_OBJECT*)psProj, psProj->player, newDest, NULL, true, -1);
|
|
}
|
|
|
|
psProj->state = PROJ_IMPACT;
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
ASSERT(distanceExtensionFactor != 0.f, "Unitialized variable used! distanceExtensionFactor is not initialized.");
|
|
|
|
if (distanceRatio > distanceExtensionFactor || /* We've traveled our maximum range */
|
|
!mapObjIsAboveGround((BASE_OBJECT*)psProj)) /* trying to travel through terrain */
|
|
{
|
|
/* Miss due to range or height */
|
|
psProj->state = PROJ_IMPACT;
|
|
setProjectileDestination(psProj, NULL); /* miss registered if NULL target */
|
|
return;
|
|
}
|
|
|
|
/* Paint effects if visible */
|
|
if (gfxVisible(psProj))
|
|
{
|
|
switch (psStats->weaponSubClass)
|
|
{
|
|
case WSC_FLAME:
|
|
{
|
|
Vector3i pos = {psProj->pos.x, psProj->pos.z-8, psProj->pos.y};
|
|
effectGiveAuxVar(distancePercent);
|
|
addEffect(&pos, EFFECT_EXPLOSION, EXPLOSION_TYPE_FLAMETHROWER, false, NULL, 0);
|
|
} break;
|
|
case WSC_COMMAND:
|
|
case WSC_ELECTRONIC:
|
|
case WSC_EMP:
|
|
{
|
|
Vector3i pos = {psProj->pos.x, psProj->pos.z-8, psProj->pos.y};
|
|
effectGiveAuxVar(distancePercent/2);
|
|
addEffect(&pos, EFFECT_EXPLOSION, EXPLOSION_TYPE_LASER, false, NULL, 0);
|
|
} break;
|
|
case WSC_ROCKET:
|
|
case WSC_MISSILE:
|
|
case WSC_SLOWROCKET:
|
|
case WSC_SLOWMISSILE:
|
|
{
|
|
Vector3i pos = {psProj->pos.x, psProj->pos.z+8, psProj->pos.y};
|
|
addEffect(&pos, EFFECT_SMOKE, SMOKE_TYPE_TRAIL, false, NULL, 0);
|
|
} break;
|
|
default:
|
|
/* add smoke trail to indirect weapons firing directly */
|
|
if (!proj_Direct(psStats))
|
|
{
|
|
Vector3i pos = {psProj->pos.x, psProj->pos.z+4, psProj->pos.y};
|
|
addEffect(&pos, EFFECT_SMOKE, SMOKE_TYPE_TRAIL, false, NULL, 0);
|
|
}
|
|
/* Otherwise no effect */
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
static void proj_InFlightIndirectFunc(PROJECTILE *psProj)
|
|
{
|
|
/* NOTE Main differences to direct projectiles:
|
|
- Movement vector / pitch update
|
|
- No extended radius
|
|
- Some optimisations by leaving out tests which are never true (homing, AA, counter-missile, lassat)
|
|
*/
|
|
|
|
int timeSoFar;
|
|
int distancePercent; /* How far we are 0..100 */
|
|
float distanceRatio; /* How far we are, 1.0==at target */
|
|
float distanceExtensionFactor; /* Extended lifespan */
|
|
Vector3i move;
|
|
unsigned int i;
|
|
WEAPON_STATS *psStats;
|
|
|
|
CHECK_PROJECTILE(psProj);
|
|
|
|
timeSoFar = gameTime - psProj->born;
|
|
|
|
psStats = psProj->psWStats;
|
|
ASSERT(psStats != NULL, "proj_InFlightIndirectFunc: Invalid weapon stats pointer");
|
|
|
|
/* Calculate extended lifespan where appropriate */
|
|
distanceExtensionFactor = 1.2f;
|
|
|
|
/* Do movement */
|
|
{
|
|
unsigned int targetDistance, currentDistance;
|
|
|
|
/* Calculate movement vector: */
|
|
move.x = psProj->tarX - psProj->startX;
|
|
move.y = psProj->tarY - psProj->startY;
|
|
move.z = (psProj->vZ - (timeSoFar * ACC_GRAVITY / (GAME_TICKS_PER_SEC * 2))) * timeSoFar / GAME_TICKS_PER_SEC; // '2' because we reach our highest point in the mid of flight, when "vZ is 0".
|
|
|
|
targetDistance = sqrtf(move.x*move.x + move.y*move.y);
|
|
currentDistance = timeSoFar * psProj->vXY / GAME_TICKS_PER_SEC; // FIXME ARTILLERY
|
|
|
|
// Prevent div by zero:
|
|
if (targetDistance == 0)
|
|
targetDistance = 1;
|
|
|
|
distanceRatio = (float)currentDistance / targetDistance;
|
|
distancePercent = PERCENT(currentDistance, targetDistance);
|
|
|
|
{
|
|
/* Calculate next position */
|
|
Vector3uw nextPos = {
|
|
psProj->startX + (distanceRatio * move.x),
|
|
psProj->startY + (distanceRatio * move.y),
|
|
psProj->srcHeight + move.z
|
|
};
|
|
|
|
/* impact if about to go off map else update coordinates */
|
|
if (!worldOnMap(nextPos.x, nextPos.y))
|
|
{
|
|
psProj->state = PROJ_IMPACT;
|
|
setProjectileDestination(psProj, NULL);
|
|
debug(LOG_NEVER, "**** projectile(%i) off map - removed ****\n", psProj->id);
|
|
return;
|
|
}
|
|
|
|
/* Update position */
|
|
psProj->pos = nextPos;
|
|
}
|
|
}
|
|
|
|
/* Update pitch */
|
|
{
|
|
float fVVert = psProj->vZ - (timeSoFar * ACC_GRAVITY / GAME_TICKS_PER_SEC);
|
|
psProj->pitch = rad2degf(atan2f(fVVert, psProj->vXY));
|
|
}
|
|
|
|
/* Check nearby objects for possible collisions */
|
|
for (i = 0; i < numProjNaybors; i++)
|
|
{
|
|
BASE_OBJECT *psTempObj = asProjNaybors[i].psObj;
|
|
|
|
CHECK_OBJECT(psTempObj);
|
|
|
|
if (psTempObj == psProj->psDamaged)
|
|
{
|
|
// Dont damage one target twice
|
|
continue;
|
|
}
|
|
|
|
if (psTempObj->died)
|
|
{
|
|
// Do not damage dead objects further
|
|
continue;
|
|
}
|
|
|
|
if (psTempObj->type == OBJ_PROJECTILE)
|
|
{
|
|
// A projectile should not collide with another projectile unless it's a counter-missile weapon
|
|
// FIXME Indirectly firing weapons are never counter-missiles?
|
|
continue;
|
|
}
|
|
|
|
if (psTempObj->type == OBJ_FEATURE &&
|
|
!((FEATURE*)psTempObj)->psStats->damageable)
|
|
{
|
|
// Ignore oil resources, artifacts and other pickups
|
|
continue;
|
|
}
|
|
|
|
if (psTempObj->player == psProj->player ||
|
|
aiCheckAlliances(psTempObj->player, psProj->player))
|
|
{
|
|
// No friendly fire
|
|
continue;
|
|
}
|
|
|
|
/* Actual collision test */
|
|
{
|
|
// FIXME HACK Needed since we got those ugly Vector3uw floating around in BASE_OBJECT...
|
|
Vector3i
|
|
posProj = {psProj->pos.x, psProj->pos.y, psProj->pos.z},
|
|
posTemp = {psTempObj->pos.x, psTempObj->pos.y, psTempObj->pos.z};
|
|
|
|
Vector3i diff = Vector3i_Sub(posProj, posTemp);
|
|
|
|
unsigned int targetHeight = establishTargetHeight(psTempObj);
|
|
unsigned int targetRadius = establishTargetRadius(psTempObj);
|
|
|
|
/* Height is always positive */
|
|
diff.z = abs(diff.z);
|
|
|
|
/* We hit! */
|
|
if (diff.z < targetHeight &&
|
|
(diff.x*diff.x + diff.y*diff.y) < targetRadius * targetRadius)
|
|
{
|
|
setProjectileDestination(psProj, psTempObj);
|
|
|
|
/* Buildings cannot be penetrated and we need a penetrating weapon */
|
|
if (psTempObj->type == OBJ_DROID && psStats->penetrate)
|
|
{
|
|
WEAPON asWeap = {psStats - asWeaponStats, 0, 0, 0};
|
|
// Determine position to fire a missile at
|
|
// (must be at least 0 because we don't use signed integers
|
|
// this shouldn't be larger than the height and width of the map either)
|
|
Vector3i newDest = {
|
|
psProj->startX + move.x * distanceExtensionFactor,
|
|
psProj->startY + move.y * distanceExtensionFactor,
|
|
psProj->srcHeight + move.z * distanceExtensionFactor
|
|
};
|
|
|
|
newDest.x = clip(newDest.x, 0, world_coord(mapWidth - 1));
|
|
newDest.y = clip(newDest.y, 0, world_coord(mapHeight - 1));
|
|
|
|
// Assume we damaged the chosen target
|
|
setProjectileDamaged(psProj, psTempObj);
|
|
|
|
proj_SendProjectile(&asWeap, (BASE_OBJECT*)psProj, psProj->player, newDest, NULL, true, -1);
|
|
}
|
|
|
|
psProj->state = PROJ_IMPACT;
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (distanceRatio > distanceExtensionFactor || /* We've traveled our maximum range */
|
|
!mapObjIsAboveGround((BASE_OBJECT*)psProj)) /* trying to travel through terrain */
|
|
{
|
|
/* Miss due to range or height */
|
|
psProj->state = PROJ_IMPACT;
|
|
setProjectileDestination(psProj, NULL); /* miss registered if NULL target */
|
|
return;
|
|
}
|
|
|
|
/* Paint effects if visible */
|
|
if (gfxVisible(psProj))
|
|
{
|
|
switch (psStats->weaponSubClass)
|
|
{
|
|
case WSC_FLAME:
|
|
{
|
|
Vector3i pos = {psProj->pos.x, psProj->pos.z-8, psProj->pos.y};
|
|
effectGiveAuxVar(distancePercent);
|
|
addEffect(&pos, EFFECT_EXPLOSION, EXPLOSION_TYPE_FLAMETHROWER, false, NULL, 0);
|
|
} break;
|
|
case WSC_COMMAND:
|
|
case WSC_ELECTRONIC:
|
|
case WSC_EMP:
|
|
{
|
|
Vector3i pos = {psProj->pos.x, psProj->pos.z-8, psProj->pos.y};
|
|
effectGiveAuxVar(distancePercent/2);
|
|
addEffect(&pos, EFFECT_EXPLOSION, EXPLOSION_TYPE_LASER, false, NULL, 0);
|
|
} break;
|
|
case WSC_ROCKET:
|
|
case WSC_MISSILE:
|
|
case WSC_SLOWROCKET:
|
|
case WSC_SLOWMISSILE:
|
|
{
|
|
Vector3i pos = {psProj->pos.x, psProj->pos.z+8, psProj->pos.y};
|
|
addEffect(&pos, EFFECT_SMOKE, SMOKE_TYPE_TRAIL, false, NULL, 0);
|
|
} break;
|
|
default:
|
|
{
|
|
Vector3i pos = {psProj->pos.x, psProj->pos.z+4, psProj->pos.y};
|
|
addEffect(&pos, EFFECT_SMOKE, SMOKE_TYPE_TRAIL, false, NULL, 0);
|
|
} break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
static void proj_ImpactFunc( PROJECTILE *psObj )
|
|
{
|
|
WEAPON_STATS *psStats;
|
|
SDWORD i, iAudioImpactID;
|
|
DROID *psCurrD, *psNextD;
|
|
STRUCTURE *psCurrS, *psNextS;
|
|
FEATURE *psCurrF, *psNextF;
|
|
UDWORD dice;
|
|
SDWORD tarX0,tarY0, tarX1,tarY1;
|
|
SDWORD radCubed, xDiff,yDiff;
|
|
float relativeDamage;
|
|
Vector3i position,scatter;
|
|
UDWORD damage; //optimisation - were all being calculated twice on PC
|
|
//Watermelon: tarZ0,tarZ1,zDiff for AA AOE weapons;
|
|
SDWORD tarZ0,tarZ1,zDiff;
|
|
// EvilGuru: Data about the effect to be shown
|
|
EFFECT_TYPE facing;
|
|
iIMDShape *imd;
|
|
HIT_SIDE impactSide = HIT_SIDE_FRONT;
|
|
|
|
CHECK_PROJECTILE(psObj);
|
|
|
|
psStats = psObj->psWStats;
|
|
ASSERT( psStats != NULL,
|
|
"proj_ImpactFunc: Invalid weapon stats pointer" );
|
|
|
|
// Get if we are facing or not
|
|
facing = (psStats->facePlayer) ? EXPLOSION_TYPE_SPECIFIED : EXPLOSION_TYPE_NOT_FACING;
|
|
|
|
// note the attacker if any
|
|
g_pProjLastAttacker = psObj->psSource;
|
|
|
|
/* play impact audio */
|
|
if (gfxVisible(psObj))
|
|
{
|
|
if (psStats->iAudioImpactID == NO_SOUND)
|
|
{
|
|
/* play richochet if MG */
|
|
if (psObj->psDest != NULL && psObj->psWStats->weaponSubClass == WSC_MGUN
|
|
&& ONEINTHREE)
|
|
{
|
|
iAudioImpactID = ID_SOUND_RICOCHET_1 + (rand() % 3);
|
|
audio_PlayStaticTrack(psObj->psDest->pos.x, psObj->psDest->pos.y, iAudioImpactID);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (psObj->psDest == NULL)
|
|
{
|
|
audio_PlayStaticTrack(psObj->tarX, psObj->tarY, psStats->iAudioImpactID);
|
|
}
|
|
else
|
|
{
|
|
audio_PlayStaticTrack(psObj->psDest->pos.x, psObj->psDest->pos.y, psStats->iAudioImpactID);
|
|
}
|
|
}
|
|
|
|
/* Shouldn't need to do this check but the stats aren't all at a value yet... */ // FIXME
|
|
if (psStats->incenRadius && psStats->incenTime)
|
|
{
|
|
position.x = psObj->tarX;
|
|
position.z = psObj->tarY;
|
|
position.y = map_Height(position.x, position.z);
|
|
effectGiveAuxVar(psStats->incenRadius);
|
|
effectGiveAuxVarSec(psStats->incenTime);
|
|
addEffect(&position, EFFECT_FIRE, FIRE_TYPE_LOCALISED, false, NULL, 0);
|
|
}
|
|
|
|
// may want to add both a fire effect and the las sat effect
|
|
if (psStats->weaponSubClass == WSC_LAS_SAT)
|
|
{
|
|
position.x = psObj->tarX;
|
|
position.z = psObj->tarY;
|
|
position.y = map_Height(position.x, position.z);
|
|
addEffect(&position, EFFECT_SAT_LASER, SAT_LASER_STANDARD, false, NULL, 0);
|
|
if (clipXY(psObj->tarX, psObj->tarY))
|
|
{
|
|
shakeStart();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set the effects position and radius
|
|
position.x = psObj->pos.x;
|
|
position.z = psObj->pos.y;
|
|
position.y = psObj->pos.z;//map_Height(psObj->pos.x, psObj->pos.y) + 24;
|
|
scatter.x = psStats->radius;
|
|
scatter.y = 0;
|
|
scatter.z = psStats->radius;
|
|
|
|
// If the projectile missed its target (or the target died)
|
|
if (psObj->psDest == NULL)
|
|
{
|
|
if (gfxVisible(psObj))
|
|
{
|
|
// The graphic to show depends on if we hit water or not
|
|
if (terrainType(mapTile(map_coord(psObj->pos.x), map_coord(psObj->pos.y))) == TER_WATER)
|
|
{
|
|
imd = psStats->pWaterHitGraphic;
|
|
}
|
|
// We did not hit water, the regular miss graphic will do the trick
|
|
else
|
|
{
|
|
imd = psStats->pTargetMissGraphic;
|
|
}
|
|
|
|
addMultiEffect(&position, &scatter, EFFECT_EXPLOSION, facing, true, imd, psStats->numExplosions, psStats->lightWorld, psStats->effectSize);
|
|
|
|
// If the target was a VTOL hit in the air add smoke
|
|
if (psObj->airTarget
|
|
&& (psStats->surfaceToAir & SHOOT_IN_AIR)
|
|
&& !(psStats->surfaceToAir & SHOOT_ON_GROUND))
|
|
{
|
|
addMultiEffect(&position, &scatter, EFFECT_SMOKE, SMOKE_TYPE_DRIFTING, false, NULL, 3, 0, 0);
|
|
}
|
|
}
|
|
}
|
|
// The projectile hit its intended target
|
|
else
|
|
{
|
|
CHECK_OBJECT(psObj->psDest);
|
|
|
|
if (psObj->psDest->type == OBJ_FEATURE
|
|
&& ((FEATURE *)psObj->psDest)->psStats->damageable == 0)
|
|
{
|
|
debug(LOG_NEVER, "proj_ImpactFunc: trying to damage non-damageable target,projectile removed");
|
|
psObj->died = gameTime;
|
|
return;
|
|
}
|
|
|
|
if (gfxVisible(psObj))
|
|
{
|
|
// If we hit a VTOL with an AA gun use the miss graphic and add some smoke
|
|
if (psObj->airTarget
|
|
&& (psStats->surfaceToAir & SHOOT_IN_AIR)
|
|
&& !(psStats->surfaceToAir & SHOOT_ON_GROUND)
|
|
&& psStats->weaponSubClass == WSC_AAGUN)
|
|
{
|
|
imd = psStats->pTargetMissGraphic;
|
|
addMultiEffect(&position, &scatter, EFFECT_SMOKE, SMOKE_TYPE_DRIFTING, false, NULL, 3, 0, 0);
|
|
}
|
|
// Otherwise we just hit it plain and simple
|
|
else
|
|
{
|
|
imd = psStats->pTargetHitGraphic;
|
|
}
|
|
|
|
addMultiEffect(&position, &scatter, EFFECT_EXPLOSION, facing, true, imd, psStats->numExplosions, psStats->lightWorld, psStats->effectSize);
|
|
}
|
|
|
|
// Check for electronic warfare damage where we know the subclass and source
|
|
if (proj_Direct(psStats)
|
|
&& psStats->weaponSubClass == WSC_ELECTRONIC
|
|
&& psObj->psSource)
|
|
{
|
|
// If we did enough `damage' to capture the target
|
|
if (electronicDamage(psObj->psDest,
|
|
calcDamage(weaponDamage(psStats, psObj->player), psStats->weaponEffect, psObj->psDest),
|
|
psObj->player))
|
|
{
|
|
switch (psObj->psSource->type)
|
|
{
|
|
case OBJ_DROID:
|
|
((DROID *) psObj->psSource)->order = DORDER_NONE;
|
|
actionDroid((DROID *) (psObj->psSource), DACTION_NONE);
|
|
break;
|
|
|
|
case OBJ_STRUCTURE:
|
|
((STRUCTURE *) psObj->psSource)->psTarget[0] = NULL;
|
|
break;
|
|
|
|
// This is only here to prevent the compiler from producing
|
|
// warnings for unhandled enumeration values
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Else it is just a regular weapon (direct or indirect)
|
|
else
|
|
{
|
|
// Calculate the damage the weapon does to its target
|
|
damage = calcDamage(weaponDamage(psStats, psObj->player), psStats->weaponEffect, psObj->psDest);
|
|
|
|
// If we are in a multi-player game and the attacker is our responsibility
|
|
if (bMultiPlayer && psObj->psSource && myResponsibility(psObj->psSource->player))
|
|
{
|
|
updateMultiStatsDamage(psObj->psSource->player, psObj->psDest->player, damage);
|
|
}
|
|
|
|
debug(LOG_NEVER, "Damage to object %d, player %d\n",
|
|
psObj->psDest->id, psObj->psDest->player);
|
|
|
|
// If the target is a droid work out the side of it we hit
|
|
if (psObj->psDest->type == OBJ_DROID)
|
|
{
|
|
// For indirect weapons (e.g. artillery) just assume the side as HIT_SIDE_TOP
|
|
impactSide = proj_Direct(psStats) ? getHitSide(psObj, psObj->psDest) : HIT_SIDE_TOP;
|
|
}
|
|
|
|
// Damage the object
|
|
relativeDamage = objectDamage(psObj->psDest,damage , psStats->weaponClass,psStats->weaponSubClass, impactSide);
|
|
|
|
proj_UpdateKills(psObj, relativeDamage);
|
|
|
|
if (relativeDamage >= 0) // So long as the target wasn't killed
|
|
{
|
|
setProjectileDamaged(psObj, psObj->psDest);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the projectile does no splash damage and does not set fire to things
|
|
if ((psStats->radius == 0) && (psStats->incenTime == 0) )
|
|
{
|
|
psObj->died = gameTime;
|
|
return;
|
|
}
|
|
|
|
if (psStats->radius != 0)
|
|
{
|
|
/* An area effect bullet */
|
|
psObj->state = PROJ_POSTIMPACT;
|
|
|
|
/* Note when it exploded for the explosion effect */
|
|
psObj->born = gameTime;
|
|
|
|
/* Work out the bounding box for the blast radius */
|
|
tarX0 = (SDWORD)psObj->pos.x - (SDWORD)psStats->radius;
|
|
tarY0 = (SDWORD)psObj->pos.y - (SDWORD)psStats->radius;
|
|
tarX1 = (SDWORD)psObj->pos.x + (SDWORD)psStats->radius;
|
|
tarY1 = (SDWORD)psObj->pos.y + (SDWORD)psStats->radius;
|
|
/* Watermelon:height bounding box for airborne units*/
|
|
tarZ0 = (SDWORD)psObj->pos.z - (SDWORD)psStats->radius;
|
|
tarZ1 = (SDWORD)psObj->pos.z + (SDWORD)psStats->radius;
|
|
|
|
/* Store the radius cubed */
|
|
radCubed = psStats->radius * psStats->radius * psStats->radius;
|
|
|
|
for (i = 0; i < MAX_PLAYERS; i++)
|
|
{
|
|
for (psCurrD = apsDroidLists[i]; psCurrD; psCurrD = psNextD)
|
|
{
|
|
/* have to store the next pointer as psCurrD could be destroyed */
|
|
psNextD = psCurrD->psNext;
|
|
|
|
/* see if psCurrD is hit (don't hit main target twice) */
|
|
if (((BASE_OBJECT *)psCurrD != psObj->psDest) &&
|
|
((SDWORD)psCurrD->pos.x >= tarX0) &&
|
|
((SDWORD)psCurrD->pos.x <= tarX1) &&
|
|
((SDWORD)psCurrD->pos.y >= tarY0) &&
|
|
((SDWORD)psCurrD->pos.y <= tarY1) &&
|
|
((SDWORD)psCurrD->pos.z >= tarZ0) &&
|
|
((SDWORD)psCurrD->pos.z <= tarZ1))
|
|
{
|
|
/* Within the bounding box, now check the radius */
|
|
xDiff = psCurrD->pos.x - psObj->pos.x;
|
|
yDiff = psCurrD->pos.y - psObj->pos.y;
|
|
zDiff = psCurrD->pos.z - psObj->pos.z;
|
|
if ((xDiff*xDiff + yDiff*yDiff + zDiff*zDiff) <= radCubed)
|
|
{
|
|
HIT_ROLL(dice);
|
|
if (dice < weaponRadiusHit(psStats, psObj->player))
|
|
{
|
|
debug(LOG_NEVER, "Damage to object %d, player %d\n",
|
|
psCurrD->id, psCurrD->player);
|
|
|
|
damage = calcDamage(
|
|
weaponRadDamage(psStats, psObj->player),
|
|
psStats->weaponEffect, (BASE_OBJECT *)psCurrD);
|
|
if (bMultiPlayer)
|
|
{
|
|
if (psObj->psSource && myResponsibility(psObj->psSource->player))
|
|
{
|
|
updateMultiStatsDamage(psObj->psSource->player, psCurrD->player, damage);
|
|
}
|
|
turnOffMultiMsg(true);
|
|
}
|
|
|
|
//Watermelon:uses a slightly different check for angle,
|
|
// since fragment of a project is from the explosion spot not from the projectile start position
|
|
impactSide = getHitSide(psObj, (BASE_OBJECT *)psCurrD);
|
|
|
|
relativeDamage = droidDamage(psCurrD, damage, psStats->weaponClass, psStats->weaponSubClass, impactSide);
|
|
|
|
turnOffMultiMsg(false); // multiplay msgs back on.
|
|
|
|
proj_UpdateKills(psObj, relativeDamage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!psObj->airTarget)
|
|
{
|
|
for (psCurrS = apsStructLists[i]; psCurrS; psCurrS = psNextS)
|
|
{
|
|
/* have to store the next pointer as psCurrD could be destroyed */
|
|
psNextS = psCurrS->psNext;
|
|
|
|
/* see if psCurrS is hit (don't hit main target twice) */
|
|
if (((BASE_OBJECT *)psCurrS != psObj->psDest) &&
|
|
((SDWORD)psCurrS->pos.x >= tarX0) &&
|
|
((SDWORD)psCurrS->pos.x <= tarX1) &&
|
|
((SDWORD)psCurrS->pos.y >= tarY0) &&
|
|
((SDWORD)psCurrS->pos.y <= tarY1))
|
|
{
|
|
/* Within the bounding box, now check the radius */
|
|
xDiff = psCurrS->pos.x - psObj->pos.x;
|
|
yDiff = psCurrS->pos.y - psObj->pos.y;
|
|
if ((xDiff*xDiff + yDiff*yDiff) <= radCubed)
|
|
{
|
|
HIT_ROLL(dice);
|
|
if (dice < weaponRadiusHit(psStats, psObj->player))
|
|
{
|
|
damage = calcDamage(weaponRadDamage(psStats, psObj->player),
|
|
psStats->weaponEffect,
|
|
(BASE_OBJECT *)psCurrS);
|
|
|
|
if (bMultiPlayer)
|
|
{
|
|
if (psObj->psSource && myResponsibility(psObj->psSource->player))
|
|
{
|
|
updateMultiStatsDamage(psObj->psSource->player, psCurrS->player,damage);
|
|
}
|
|
}
|
|
|
|
//Watermelon:uses a slightly different check for angle,
|
|
// since fragment of a project is from the explosion spot not from the projectile start position
|
|
impactSide = getHitSide(psObj, (BASE_OBJECT *)psCurrS);
|
|
|
|
relativeDamage = structureDamage(psCurrS,
|
|
damage,
|
|
psStats->weaponClass,
|
|
psStats->weaponSubClass, impactSide);
|
|
|
|
proj_UpdateKills(psObj, relativeDamage);
|
|
}
|
|
}
|
|
}
|
|
// Missed by old method, but maybe in landed within the building's footprint(baseplate)
|
|
else if(ptInStructure(psCurrS,psObj->pos.x, psObj->pos.y) && (BASE_OBJECT*)psCurrS != psObj->psDest)
|
|
{
|
|
damage = NOMINAL_DAMAGE;
|
|
|
|
if(bMultiPlayer)
|
|
{
|
|
if(psObj->psSource && myResponsibility(psObj->psSource->player))
|
|
{
|
|
updateMultiStatsDamage(psObj->psSource->player, psCurrS->player,damage);
|
|
}
|
|
}
|
|
|
|
relativeDamage = structureDamage(psCurrS,
|
|
damage,
|
|
psStats->weaponClass,
|
|
psStats->weaponSubClass, impactSide);
|
|
|
|
proj_UpdateKills(psObj, relativeDamage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (psCurrF = apsFeatureLists[0]; psCurrF; psCurrF = psNextF)
|
|
{
|
|
/* have to store the next pointer as psCurrD could be destroyed */
|
|
psNextF = psCurrF->psNext;
|
|
|
|
//ignore features that are not damageable
|
|
if(!psCurrF->psStats->damageable)
|
|
{
|
|
continue;
|
|
}
|
|
/* see if psCurrS is hit (don't hit main target twice) */
|
|
if (((BASE_OBJECT *)psCurrF != psObj->psDest) &&
|
|
((SDWORD)psCurrF->pos.x >= tarX0) &&
|
|
((SDWORD)psCurrF->pos.x <= tarX1) &&
|
|
((SDWORD)psCurrF->pos.y >= tarY0) &&
|
|
((SDWORD)psCurrF->pos.y <= tarY1))
|
|
{
|
|
/* Within the bounding box, now check the radius */
|
|
xDiff = psCurrF->pos.x - psObj->pos.x;
|
|
yDiff = psCurrF->pos.y - psObj->pos.y;
|
|
if ((xDiff*xDiff + yDiff*yDiff) <= radCubed)
|
|
{
|
|
HIT_ROLL(dice);
|
|
if (dice < weaponRadiusHit(psStats, psObj->player))
|
|
{
|
|
debug(LOG_NEVER, "Damage to object %d, player %d\n",
|
|
psCurrF->id, psCurrF->player);
|
|
|
|
// Watermelon:uses a slightly different check for angle,
|
|
// since fragment of a project is from the explosion spot not from the projectile start position
|
|
impactSide = getHitSide(psObj, (BASE_OBJECT *)psCurrF);
|
|
|
|
relativeDamage = featureDamage(psCurrF,
|
|
calcDamage(weaponRadDamage(psStats, psObj->player),
|
|
psStats->weaponEffect,
|
|
(BASE_OBJECT *)psCurrF),
|
|
psStats->weaponClass,
|
|
psStats->weaponSubClass, impactSide);
|
|
|
|
proj_UpdateKills(psObj, relativeDamage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (psStats->incenTime != 0)
|
|
{
|
|
/* Incendiary round */
|
|
/* Incendiary damage gets done in the bullet update routine */
|
|
/* Just note when the incendiary started burning */
|
|
psObj->state = PROJ_POSTIMPACT;
|
|
psObj->born = gameTime;
|
|
}
|
|
/* Something was blown up */
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
static void proj_PostImpactFunc( PROJECTILE *psObj )
|
|
{
|
|
WEAPON_STATS *psStats;
|
|
SDWORD i, age;
|
|
FIRE_BOX flame;
|
|
|
|
CHECK_PROJECTILE(psObj);
|
|
|
|
psStats = psObj->psWStats;
|
|
ASSERT( psStats != NULL,
|
|
"proj_PostImpactFunc: Invalid weapon stats pointer" );
|
|
|
|
age = (SDWORD)gameTime - (SDWORD)psObj->born;
|
|
|
|
/* Time to finish postimpact effect? */
|
|
if (age > (SDWORD)psStats->radiusLife && age > (SDWORD)psStats->incenTime)
|
|
{
|
|
psObj->died = gameTime;
|
|
return;
|
|
}
|
|
|
|
/* Burning effect */
|
|
if (psStats->incenTime > 0)
|
|
{
|
|
/* See if anything is in the fire and burn it */
|
|
|
|
/* Calculate the fire's bounding box */
|
|
flame.x1 = (SWORD)(psObj->pos.x - psStats->incenRadius);
|
|
flame.y1 = (SWORD)(psObj->pos.y - psStats->incenRadius);
|
|
flame.x2 = (SWORD)(psObj->pos.x + psStats->incenRadius);
|
|
flame.y2 = (SWORD)(psObj->pos.y + psStats->incenRadius);
|
|
flame.rad = (SWORD)(psStats->incenRadius*psStats->incenRadius);
|
|
|
|
for (i=0; i<MAX_PLAYERS; i++)
|
|
{
|
|
/* Don't damage your own droids - unrealistic, but better */
|
|
if(i!=psObj->player)
|
|
{
|
|
proj_checkBurnDamage((BASE_OBJECT*)apsDroidLists[i], psObj, &flame);
|
|
proj_checkBurnDamage((BASE_OBJECT*)apsStructLists[i], psObj, &flame);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
static void proj_Update(PROJECTILE *psObj)
|
|
{
|
|
CHECK_PROJECTILE(psObj);
|
|
|
|
/* See if any of the stored objects have died
|
|
* since the projectile was created
|
|
*/
|
|
if (psObj->psSource && psObj->psSource->died)
|
|
{
|
|
setProjectileSource(psObj, NULL);
|
|
}
|
|
if (psObj->psDest && psObj->psDest->died)
|
|
{
|
|
setProjectileDestination(psObj, NULL);
|
|
}
|
|
if (psObj->psDamaged && psObj->psDamaged->died)
|
|
{
|
|
setProjectileDamaged(psObj, NULL);
|
|
}
|
|
|
|
// This extra check fixes a crash in cam2, mission1
|
|
if (worldOnMap(psObj->pos.x, psObj->pos.y) == false)
|
|
{
|
|
psObj->died = true;
|
|
return;
|
|
}
|
|
|
|
projGetNaybors((PROJECTILE *)psObj);
|
|
|
|
switch (psObj->state)
|
|
{
|
|
case PROJ_INFLIGHT:
|
|
(psObj->pInFlightFunc) ( psObj );
|
|
break;
|
|
|
|
case PROJ_IMPACT:
|
|
proj_ImpactFunc( psObj );
|
|
break;
|
|
|
|
case PROJ_POSTIMPACT:
|
|
proj_PostImpactFunc( psObj );
|
|
break;
|
|
}
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
// iterate through all projectiles and update their status
|
|
void proj_UpdateAll()
|
|
{
|
|
PROJECTILE *psObj, *psPrev;
|
|
|
|
for (psObj = psProjectileList; psObj != NULL; psObj = psObj->psNext)
|
|
{
|
|
proj_Update( psObj );
|
|
}
|
|
|
|
// Now delete any dead projectiles
|
|
psObj = psProjectileList;
|
|
|
|
// is the first node dead?
|
|
while (psObj && psObj == psProjectileList && psObj->died)
|
|
{
|
|
psProjectileList = psObj->psNext;
|
|
proj_Free(psObj);
|
|
psObj = psProjectileList;
|
|
}
|
|
|
|
// first object is now either NULL or not dead, so we have time to set this below
|
|
psPrev = NULL;
|
|
|
|
// are any in the list dead?
|
|
while (psObj)
|
|
{
|
|
if (psObj->died)
|
|
{
|
|
psPrev->psNext = psObj->psNext;
|
|
proj_Free(psObj);
|
|
psObj = psPrev->psNext;
|
|
} else {
|
|
psPrev = psObj;
|
|
psObj = psObj->psNext;
|
|
}
|
|
}
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
static void proj_checkBurnDamage( BASE_OBJECT *apsList, PROJECTILE *psProj, FIRE_BOX *pFireBox )
|
|
{
|
|
BASE_OBJECT *psCurr, *psNext;
|
|
SDWORD xDiff,yDiff;
|
|
WEAPON_STATS *psStats;
|
|
UDWORD damageSoFar;
|
|
SDWORD damageToDo;
|
|
float relativeDamage;
|
|
|
|
CHECK_PROJECTILE(psProj);
|
|
|
|
// note the attacker if any
|
|
g_pProjLastAttacker = psProj->psSource;
|
|
|
|
psStats = psProj->psWStats;
|
|
for (psCurr = apsList; psCurr; psCurr = psNext)
|
|
{
|
|
/* have to store the next pointer as psCurr could be destroyed */
|
|
psNext = psCurr->psNext;
|
|
|
|
if ((psCurr->type == OBJ_DROID) &&
|
|
isVtolDroid((DROID*)psCurr) &&
|
|
((DROID *)psCurr)->sMove.Status != MOVEINACTIVE)
|
|
{
|
|
// can't set flying vtols on fire
|
|
continue;
|
|
}
|
|
|
|
/* see if psCurr is hit (don't hit main target twice) */
|
|
if (((SDWORD)psCurr->pos.x >= pFireBox->x1) &&
|
|
((SDWORD)psCurr->pos.x <= pFireBox->x2) &&
|
|
((SDWORD)psCurr->pos.y >= pFireBox->y1) &&
|
|
((SDWORD)psCurr->pos.y <= pFireBox->y2))
|
|
{
|
|
/* Within the bounding box, now check the radius */
|
|
xDiff = psCurr->pos.x - psProj->pos.x;
|
|
yDiff = psCurr->pos.y - psProj->pos.y;
|
|
if ((xDiff*xDiff + yDiff*yDiff) <= pFireBox->rad)
|
|
{
|
|
/* The object is in the fire */
|
|
psCurr->inFire |= IN_FIRE;
|
|
|
|
if ( (psCurr->burnStart == 0) ||
|
|
(psCurr->inFire & BURNING) )
|
|
{
|
|
/* This is the first turn the object is in the fire */
|
|
psCurr->burnStart = gameTime;
|
|
psCurr->burnDamage = 0;
|
|
}
|
|
else
|
|
{
|
|
/* Calculate how much damage should have
|
|
been done up till now */
|
|
damageSoFar = (gameTime - psCurr->burnStart)
|
|
//* psStats->incenDamage
|
|
* weaponIncenDamage(psStats,psProj->player)
|
|
/ GAME_TICKS_PER_SEC;
|
|
damageToDo = (SDWORD)damageSoFar
|
|
- (SDWORD)psCurr->burnDamage;
|
|
if (damageToDo > 0)
|
|
{
|
|
debug(LOG_NEVER, "Burn damage of %d to object %d, player %d\n",
|
|
damageToDo, psCurr->id, psCurr->player);
|
|
|
|
//Watermelon:just assume the burn damage is from FRONT
|
|
relativeDamage = objectDamage(psCurr, damageToDo, psStats->weaponClass,psStats->weaponSubClass, 0);
|
|
|
|
psCurr->burnDamage += damageToDo;
|
|
|
|
proj_UpdateKills(psProj, relativeDamage);
|
|
}
|
|
/* The damage could be negative if the object
|
|
is being burn't by another fire
|
|
with a higher burn damage */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
// return whether a weapon is direct or indirect
|
|
bool proj_Direct(const WEAPON_STATS* psStats)
|
|
{
|
|
ASSERT(psStats != NULL, "proj_Direct: called with NULL weapon!");
|
|
if (!psStats)
|
|
{
|
|
return true; // arbitrary value in no-debug case
|
|
}
|
|
ASSERT(psStats->movementModel < NUM_MOVEMENT_MODEL, "proj_Direct: invalid weapon stat");
|
|
|
|
switch (psStats->movementModel)
|
|
{
|
|
case MM_DIRECT:
|
|
case MM_HOMINGDIRECT:
|
|
case MM_ERRATICDIRECT:
|
|
case MM_SWEEP:
|
|
return true;
|
|
break;
|
|
case MM_INDIRECT:
|
|
case MM_HOMINGINDIRECT:
|
|
return false;
|
|
break;
|
|
case NUM_MOVEMENT_MODEL:
|
|
break; // error checking in assert above; this is for no-debug case
|
|
}
|
|
|
|
return true; // just to satisfy compiler
|
|
}
|
|
|
|
/***************************************************************************/
|
|
|
|
// return the maximum range for a weapon
|
|
SDWORD proj_GetLongRange(const WEAPON_STATS* psStats)
|
|
{
|
|
return psStats->longRange;
|
|
}
|
|
|
|
|
|
/***************************************************************************/
|
|
static UDWORD establishTargetRadius(BASE_OBJECT *psTarget)
|
|
{
|
|
UDWORD radius;
|
|
STRUCTURE *psStructure;
|
|
FEATURE *psFeat;
|
|
|
|
CHECK_OBJECT(psTarget);
|
|
radius = 0;
|
|
|
|
switch(psTarget->type)
|
|
{
|
|
case OBJ_DROID:
|
|
switch(((DROID *)psTarget)->droidType)
|
|
{
|
|
case DROID_WEAPON:
|
|
case DROID_SENSOR:
|
|
case DROID_ECM:
|
|
case DROID_CONSTRUCT:
|
|
case DROID_COMMAND:
|
|
case DROID_REPAIR:
|
|
case DROID_PERSON:
|
|
case DROID_CYBORG:
|
|
case DROID_CYBORG_CONSTRUCT:
|
|
case DROID_CYBORG_REPAIR:
|
|
case DROID_CYBORG_SUPER:
|
|
//Watermelon:'hitbox' size is now based on imd size
|
|
radius = abs(psTarget->sDisplay.imd->radius) * 2;
|
|
break;
|
|
case DROID_DEFAULT:
|
|
case DROID_TRANSPORTER:
|
|
default:
|
|
radius = TILE_UNITS/4; // how will we arrive at this?
|
|
}
|
|
break;
|
|
case OBJ_STRUCTURE:
|
|
psStructure = (STRUCTURE*)psTarget;
|
|
radius = (MAX(psStructure->pStructureType->baseBreadth, psStructure->pStructureType->baseWidth) * TILE_UNITS) / 2;
|
|
break;
|
|
case OBJ_FEATURE:
|
|
// radius = TILE_UNITS/4; // how will we arrive at this?
|
|
psFeat = (FEATURE *)psTarget;
|
|
radius = (MAX(psFeat->psStats->baseBreadth,psFeat->psStats->baseWidth) * TILE_UNITS) / 2;
|
|
break;
|
|
case OBJ_PROJECTILE:
|
|
//Watermelon 1/2 radius of a droid?
|
|
radius = TILE_UNITS/8;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return(radius);
|
|
}
|
|
/***************************************************************************/
|
|
|
|
/*the damage depends on the weapon effect and the target propulsion type or
|
|
structure strength*/
|
|
UDWORD calcDamage(UDWORD baseDamage, WEAPON_EFFECT weaponEffect, BASE_OBJECT *psTarget)
|
|
{
|
|
UDWORD damage;
|
|
|
|
if (psTarget->type == OBJ_STRUCTURE)
|
|
{
|
|
damage = baseDamage * asStructStrengthModifier[weaponEffect][((
|
|
STRUCTURE *)psTarget)->pStructureType->strength] / 100;
|
|
}
|
|
else if (psTarget->type == OBJ_DROID)
|
|
{
|
|
damage = baseDamage * asWeaponModifier[weaponEffect][(
|
|
asPropulsionStats + ((DROID *)psTarget)->asBits[COMP_PROPULSION].
|
|
nStat)->propulsionType] / 100;
|
|
}
|
|
// Default value
|
|
else
|
|
{
|
|
damage = baseDamage;
|
|
}
|
|
|
|
// A little fail safe!
|
|
if (damage == 0 && baseDamage != 0)
|
|
{
|
|
damage = 1;
|
|
}
|
|
|
|
return damage;
|
|
}
|
|
|
|
/*
|
|
* A quick explanation about hown this function works:
|
|
* - It returns an integer between 0 and 100 (see note for exceptions);
|
|
* - this represents the amount of damage inflicted on the droid by the weapon
|
|
* in relation to its original health.
|
|
* - e.g. If 100 points of (*actual*) damage were done to a unit who started
|
|
* off (when first produced) with 400 points then .25 would be returned.
|
|
* - If the actual damage done to a unit is greater than its remaining points
|
|
* then the actual damage is clipped: so if we did 200 actual points of
|
|
* damage to a cyborg with 150 points left the actual damage would be taken
|
|
* as 150.
|
|
* - Should sufficient damage be done to destroy/kill a unit then the value is
|
|
* multiplied by -1, resulting in a negative number.
|
|
*/
|
|
static float objectDamage(BASE_OBJECT *psObj, UDWORD damage, UDWORD weaponClass,UDWORD weaponSubClass, HIT_SIDE impactSide)
|
|
{
|
|
switch (psObj->type)
|
|
{
|
|
case OBJ_DROID:
|
|
return droidDamage((DROID *)psObj, damage, weaponClass,weaponSubClass, impactSide);
|
|
break;
|
|
|
|
case OBJ_STRUCTURE:
|
|
return structureDamage((STRUCTURE *)psObj, damage, weaponClass, weaponSubClass, impactSide);
|
|
break;
|
|
|
|
case OBJ_FEATURE:
|
|
return featureDamage((FEATURE *)psObj, damage, weaponClass, weaponSubClass, impactSide);
|
|
break;
|
|
|
|
case OBJ_PROJECTILE:
|
|
ASSERT(!"invalid object type: bullet", "objectDamage: invalid object type: OBJ_PROJECTILE");
|
|
break;
|
|
|
|
case OBJ_TARGET:
|
|
ASSERT(!"invalid object type: target", "objectDamage: invalid object type: OBJ_TARGET");
|
|
break;
|
|
|
|
default:
|
|
ASSERT(!"unknown object type", "objectDamage: unknown object type");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* This function will calculate which side of the droid psTarget the projectile
|
|
* psObj hit. Although it is possible to extract the target from psObj it is
|
|
* only the `direct' target of the projectile. Since impact sides also apply for
|
|
* any splash damage a projectile might do the exact target is needed.
|
|
*/
|
|
static HIT_SIDE getHitSide(PROJECTILE *psObj, BASE_OBJECT *psTarget)
|
|
{
|
|
int deltaX, deltaY;
|
|
int impactAngle;
|
|
|
|
// If we hit the top of the droid
|
|
if (psObj->altChange > 300)
|
|
{
|
|
return HIT_SIDE_TOP;
|
|
}
|
|
// If the height difference between us and the target is > 50
|
|
else if (psObj->pos.z < (psTarget->pos.z - 50))
|
|
{
|
|
return HIT_SIDE_BOTTOM;
|
|
}
|
|
// We hit an actual `side'
|
|
else
|
|
{
|
|
deltaX = psObj->startX - psTarget->pos.x;
|
|
deltaY = psObj->startY - psTarget->pos.y;
|
|
|
|
/*
|
|
* Work out the impact angle. It is easiest to understand if you
|
|
* model the target droid as a circle, divided up into 360 pieces.
|
|
*/
|
|
impactAngle = abs(psTarget->direction - (180 * atan2f(deltaX, deltaY) / M_PI));
|
|
|
|
impactAngle = wrap(impactAngle, 360);
|
|
|
|
// Use the impact angle to work out the side hit
|
|
// Right
|
|
if (impactAngle > 45 && impactAngle < 135)
|
|
return HIT_SIDE_RIGHT;
|
|
// Rear
|
|
else if (impactAngle >= 135 && impactAngle <= 225)
|
|
return HIT_SIDE_REAR;
|
|
// Left
|
|
else if (impactAngle > 225 && impactAngle < 315)
|
|
return HIT_SIDE_LEFT;
|
|
// Front - default
|
|
else //if (impactAngle <= 45 || impactAngle >= 315)
|
|
return HIT_SIDE_FRONT;
|
|
}
|
|
}
|
|
|
|
/* Returns true if an object has just been hit by an electronic warfare weapon*/
|
|
static BOOL justBeenHitByEW(BASE_OBJECT *psObj)
|
|
{
|
|
DROID *psDroid;
|
|
FEATURE *psFeature;
|
|
STRUCTURE *psStructure;
|
|
|
|
if(gamePaused())
|
|
{
|
|
return(false); // Don't shake when paused...!
|
|
}
|
|
|
|
switch(psObj->type)
|
|
{
|
|
case OBJ_DROID:
|
|
psDroid = (DROID*)psObj;
|
|
if ((gameTime - psDroid->timeLastHit) < ELEC_DAMAGE_DURATION
|
|
&& psDroid->lastHitWeapon == WSC_ELECTRONIC)
|
|
{
|
|
return(true);
|
|
}
|
|
break;
|
|
|
|
case OBJ_FEATURE:
|
|
psFeature = (FEATURE*)psObj;
|
|
if ((gameTime - psFeature->timeLastHit) < ELEC_DAMAGE_DURATION)
|
|
{
|
|
return(true);
|
|
}
|
|
break;
|
|
|
|
case OBJ_STRUCTURE:
|
|
psStructure = (STRUCTURE*)psObj;
|
|
if ((gameTime - psStructure->timeLastHit) < ELEC_DAMAGE_DURATION
|
|
&& psStructure->lastHitWeapon == WSC_ELECTRONIC)
|
|
{
|
|
return true;
|
|
}
|
|
break;
|
|
|
|
case OBJ_PROJECTILE:
|
|
ASSERT(!"invalid object type: bullet", "justBeenHitByEW: invalid object type: OBJ_PROJECTILE");
|
|
abort();
|
|
break;
|
|
|
|
case OBJ_TARGET:
|
|
ASSERT(!"invalid object type: target", "justBeenHitByEW: invalid object type: OBJ_TARGET");
|
|
abort();
|
|
break;
|
|
|
|
default:
|
|
ASSERT(!"unknown object type", "justBeenHitByEW: unknown object type");
|
|
abort();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void objectShimmy(BASE_OBJECT *psObj)
|
|
{
|
|
if(justBeenHitByEW(psObj))
|
|
{
|
|
iV_MatrixRotateX(SKY_SHIMMY);
|
|
iV_MatrixRotateY(SKY_SHIMMY);
|
|
iV_MatrixRotateZ(SKY_SHIMMY);
|
|
if(psObj->type == OBJ_DROID)
|
|
{
|
|
iV_TRANSLATE(1-rand()%3,0,1-rand()%3);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Watermelon:addProjNaybor ripped from droid.c
|
|
/* Add a new object to the projectile naybor list */
|
|
static void addProjNaybor(BASE_OBJECT *psObj, UDWORD distSqr)
|
|
{
|
|
UDWORD pos;
|
|
|
|
if (numProjNaybors == 0)
|
|
{
|
|
// No objects in the list
|
|
asProjNaybors[0].psObj = psObj;
|
|
asProjNaybors[0].distSqr = distSqr;
|
|
numProjNaybors++;
|
|
}
|
|
else if (distSqr >= asProjNaybors[numProjNaybors-1].distSqr)
|
|
{
|
|
// Simple case - this is the most distant object
|
|
asProjNaybors[numProjNaybors].psObj = psObj;
|
|
asProjNaybors[numProjNaybors].distSqr = distSqr;
|
|
numProjNaybors++;
|
|
}
|
|
else
|
|
{
|
|
// Move all the objects further away up the list
|
|
pos = numProjNaybors;
|
|
while (pos > 0 && asProjNaybors[pos - 1].distSqr > distSqr)
|
|
{
|
|
memcpy(asProjNaybors + pos, asProjNaybors + (pos - 1), sizeof(PROJ_NAYBOR_INFO));
|
|
pos --;
|
|
}
|
|
|
|
// Insert the object at the correct position
|
|
asProjNaybors[pos].psObj = psObj;
|
|
asProjNaybors[pos].distSqr = distSqr;
|
|
numProjNaybors++;
|
|
}
|
|
}
|
|
|
|
//Watermelon: projGetNaybors ripped from droid.c
|
|
/* Find all the objects close to the projectile */
|
|
static void projGetNaybors(PROJECTILE *psObj)
|
|
{
|
|
SDWORD xdiff, ydiff;
|
|
UDWORD dx,dy, distSqr;
|
|
BASE_OBJECT *psTempObj;
|
|
|
|
CHECK_PROJECTILE(psObj);
|
|
|
|
// Ensure only called max of once per droid per game cycle.
|
|
if (CurrentProjNaybors == (BASE_OBJECT *)psObj && projnayborTime == gameTime)
|
|
{
|
|
return;
|
|
}
|
|
CurrentProjNaybors = (BASE_OBJECT *)psObj;
|
|
projnayborTime = gameTime;
|
|
|
|
// reset the naybor array
|
|
numProjNaybors = 0;
|
|
|
|
// search for naybor objects
|
|
dx = ((BASE_OBJECT *)psObj)->pos.x;
|
|
dy = ((BASE_OBJECT *)psObj)->pos.y;
|
|
|
|
gridStartIterate((SDWORD)dx, (SDWORD)dy);
|
|
for (psTempObj = gridIterate(); psTempObj != NULL; psTempObj = gridIterate())
|
|
{
|
|
if (psTempObj != (BASE_OBJECT *)psObj && !psTempObj->died)
|
|
{
|
|
// see if an object is in NAYBOR_RANGE
|
|
xdiff = dx - (SDWORD)psTempObj->pos.x;
|
|
if (xdiff < 0)
|
|
{
|
|
xdiff = -xdiff;
|
|
}
|
|
if (xdiff > PROJ_NAYBOR_RANGE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
ydiff = dy - (SDWORD)psTempObj->pos.y;
|
|
if (ydiff < 0)
|
|
{
|
|
ydiff = -ydiff;
|
|
}
|
|
if (ydiff > PROJ_NAYBOR_RANGE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
distSqr = xdiff*xdiff + ydiff*ydiff;
|
|
if (distSqr > PROJ_NAYBOR_RANGE*PROJ_NAYBOR_RANGE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
addProjNaybor(psTempObj, distSqr);
|
|
if (numProjNaybors >= MAX_NAYBORS)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static UDWORD establishTargetHeight(BASE_OBJECT *psTarget)
|
|
{
|
|
UDWORD height;
|
|
UDWORD utilityHeight = 0, yMax = 0, yMin = 0; // Temporaries for addition of utility's height to total height
|
|
DROID *psDroid;
|
|
STRUCTURE_STATS *psStructureStats;
|
|
|
|
if (psTarget == NULL)
|
|
{
|
|
return 0;
|
|
}
|
|
CHECK_OBJECT(psTarget);
|
|
|
|
switch(psTarget->type)
|
|
{
|
|
case OBJ_DROID:
|
|
psDroid = (DROID*)psTarget;
|
|
height = asBodyStats[psDroid->asBits[COMP_BODY].nStat].pIMD->max.y - asBodyStats[psDroid->asBits[COMP_BODY].nStat].pIMD->min.y;
|
|
|
|
// Don't do this for Barbarian Propulsions as they don't possess a turret (and thus have pIMD == NULL)
|
|
if (!strcmp(asPropulsionStats[psDroid->asBits[COMP_PROPULSION].nStat].pName, "BaBaProp") )
|
|
{
|
|
return height;
|
|
}
|
|
|
|
// Commanders don't have pIMD either
|
|
if (psDroid->droidType == DROID_COMMAND)
|
|
return height;
|
|
|
|
// VTOL's don't have pIMD either it seems...
|
|
if (isVtolDroid(psDroid))
|
|
{
|
|
return (height + VTOL_HITBOX_MODIFICATOR);
|
|
}
|
|
|
|
switch(psDroid->droidType)
|
|
{
|
|
case DROID_WEAPON:
|
|
if ( psDroid->numWeaps > 0 )
|
|
{
|
|
yMax = (asWeaponStats[psDroid->asWeaps[0].nStat]).pIMD->max.y;
|
|
yMin = (asWeaponStats[psDroid->asWeaps[0].nStat]).pIMD->min.y;
|
|
}
|
|
break;
|
|
|
|
case DROID_SENSOR:
|
|
yMax = (asSensorStats[psDroid->asBits[COMP_SENSOR].nStat]).pIMD->max.y;
|
|
yMin = (asSensorStats[psDroid->asBits[COMP_SENSOR].nStat]).pIMD->min.y;
|
|
break;
|
|
|
|
case DROID_ECM:
|
|
yMax = (asECMStats[psDroid->asBits[COMP_ECM].nStat]).pIMD->max.y;
|
|
yMin = (asECMStats[psDroid->asBits[COMP_ECM].nStat]).pIMD->min.y;
|
|
break;
|
|
|
|
case DROID_CONSTRUCT:
|
|
yMax = (asConstructStats[psDroid->asBits[COMP_CONSTRUCT].nStat]).pIMD->max.y;
|
|
yMin = (asConstructStats[psDroid->asBits[COMP_CONSTRUCT].nStat]).pIMD->min.y;
|
|
break;
|
|
|
|
case DROID_REPAIR:
|
|
yMax = (asRepairStats[psDroid->asBits[COMP_REPAIRUNIT].nStat]).pIMD->max.y;
|
|
yMin = (asRepairStats[psDroid->asBits[COMP_REPAIRUNIT].nStat]).pIMD->min.y;
|
|
break;
|
|
|
|
case DROID_PERSON:
|
|
//TODO:add person 'state'checks here(stand, knee, crouch, prone etc)
|
|
case DROID_CYBORG:
|
|
case DROID_CYBORG_CONSTRUCT:
|
|
case DROID_CYBORG_REPAIR:
|
|
case DROID_CYBORG_SUPER:
|
|
case DROID_DEFAULT:
|
|
case DROID_TRANSPORTER:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
utilityHeight = (yMax + yMin)/2;
|
|
height += utilityHeight;
|
|
|
|
return height;
|
|
|
|
case OBJ_STRUCTURE:
|
|
psStructureStats = ((STRUCTURE *)psTarget)->pStructureType;
|
|
return (psStructureStats->pIMD->max.y + psStructureStats->pIMD->min.y) / 2;
|
|
case OBJ_FEATURE:
|
|
// Just use imd ymax+ymin
|
|
return (psTarget->sDisplay.imd->max.y + psTarget->sDisplay.imd->min.y) / 2;
|
|
case OBJ_PROJECTILE:
|
|
// 16 for bullet
|
|
return 16;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|