/* =========================================================================== Wolfenstein: Enemy Territory GPL Source Code Copyright (C) 1999-2010 id Software LLC, a ZeniMax Media company. This file is part of the Wolfenstein: Enemy Territory GPL Source Code (“Wolf ET Source Code”). Wolf ET Source Code 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. Wolf ET Source Code 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 Wolf ET Source Code. If not, see . In addition, the Wolf: ET Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Wolf ET Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA. =========================================================================== */ // cg_flamethrower.c - special code for the flamethrower effects // // the flameChunks behave similarly to the trailJunc's, except they are rendered differently, and // also interact with the environment // // NOTE: some AI's are treated different, mostly for aesthetical reasons. #include "cg_local.h" // a flameChunk is a ball or section of fuel which goes from fuel->blue ignition->flame ball // optimization is necessary, since lots of these will be spawned, but as they grow, they can be // merged so that less overdraw occurs typedef struct flameChunk_s { struct flameChunk_s *nextGlobal, *prevGlobal; // next junction in the global list it is in (free or used) struct flameChunk_s *nextFlameChunk; // next junction in the trail struct flameChunk_s *nextHead, *prevHead; // next head junc in the world qboolean inuse; qboolean dead; // set when a chunk is effectively inactive, but waiting to be freed int ownerCent; // cent that spawned us int timeStart, timeEnd; float sizeMax; // start small, increase if we slow down float sizeRand; float sizeRate; // rate per ms, variable according to speed (larger if moving slower) vec3_t baseOrg; int baseOrgTime; vec3_t velDir; float velSpeed; // flame chunks should start with a fast velocity, then slow down if there is nothing behind them pushing them along float rollAngle; qboolean ignitionOnly; int blueLife; float gravity; vec3_t startVelDir; float speedScale; // current variables vec3_t org; float size; float lifeFrac; // 0.0 (baby) -> 1.0 (aged) int lastFriction, lastFrictionTake; vec3_t parentFwd; } flameChunk_t; // DHM - Nerve :: lowered this from 2048. Still allows 6-9 people flaming. #define MAX_FLAME_CHUNKS 1024 static flameChunk_t flameChunks[MAX_FLAME_CHUNKS]; static flameChunk_t *freeFlameChunks, *activeFlameChunks, *headFlameChunks; static qboolean initFlameChunks = qfalse; static int numFlameChunksInuse; // this structure stores information relevant to each cent in the game, this way we keep // the flamethrower data seperate to the rest of the code, which helps if we decide against // using this weapon in the game typedef struct centFlameInfo_s { int lastClientFrame; // client frame that we last fired the flamethrower vec3_t lastAngles; // angles at last firing vec3_t lastOrigin; // origin at last firing flameChunk_t *lastFlameChunk; // flame chunk we last spawned int lastSoundUpdate; qboolean lastFiring; int lastDmgUpdate; // time we last told server about this ent's flame damage int lastDmgCheck; // only check once per 100ms int lastDmgEnemy; // entity that inflicted the damage } centFlameInfo_t; static centFlameInfo_t centFlameInfo[MAX_GENTITIES]; typedef struct { // float fireVolume; // not needed, since we add individual loop sources for the flame, so it gets spacialized float blowVolume; float streamVolume; } flameSoundStatus_t; static flameSoundStatus_t centFlameStatus[MAX_GENTITIES]; // procedure defs flameChunk_t *CG_SpawnFlameChunk( flameChunk_t *headFlameChunk ); void CG_FlameCalcOrg( flameChunk_t *f, int time, vec3_t outOrg ); void CG_FlameGetMuzzlePoint( vec3_t org, vec3_t fwd, vec3_t right, vec3_t up, vec3_t outPos ); // these must be globals, since they cannot expand or contract, since that might result in them getting // stuck in geometry. therefore when a chunk hits a surface, we should deflect it away from the surface // slightly, rather than running along it, so that as the chink grows, the sprites don't sink into the // wall too much. static vec3_t flameChunkMins = {0, 0, 0}; static vec3_t flameChunkMaxs = {0, 0, 0}; // these define how the flame looks #define FLAME_START_SIZE 1.0 #define FLAME_START_MAX_SIZE 140.0 // when the flame is spawned, it should endevour to reach this size #define FLAME_START_MAX_SIZE_RAND 60.0 #define FLAME_MAX_SIZE 200.0 // flame sprites cannot be larger than this #define FLAME_MIN_MAXSIZE 40.0 // don't ever let the sizeMax go less than this #define FLAME_START_SPEED 1200.0 //1200.0 // speed of flame as it leaves the nozzle #define FLAME_MIN_SPEED 60.0 //200.0 #define FLAME_CHUNK_DIST 8.0 // space in between chunks when fired #define FLAME_BLUE_LENGTH 130.0 #define FLAME_BLUE_MAX_ALPHA 1.0 #define FLAME_FUEL_LENGTH 48.0 #define FLAME_FUEL_MAX_ALPHA 0.35 #define FLAME_FUEL_MIN_WIDTH 1.0 // these are calculated (don't change) #define FLAME_LENGTH ( FLAMETHROWER_RANGE + 50.0 ) // NOTE: only modify the range, since this should always reflect that range #define FLAME_LIFETIME (int)( ( FLAME_LENGTH / FLAME_START_SPEED ) * 1000 ) // life duration in milliseconds #define FLAME_FRICTION_PER_SEC ( 2.0 * FLAME_START_SPEED ) #define FLAME_BLUE_LIFE (int)( ( FLAME_BLUE_LENGTH / FLAME_START_SPEED ) * 1000 ) #define FLAME_FUEL_LIFE (int)( ( FLAME_FUEL_LENGTH / FLAME_START_SPEED ) * 1000 ) #define FLAME_FUEL_FADEIN_TIME ( 0.2 * FLAME_FUEL_LIFE ) #define FLAME_BLUE_FADEIN_TIME( x ) ( 0.2 * x ) #define FLAME_BLUE_FADEOUT_TIME( x ) ( 0.05 * x ) #define GET_FLAME_BLUE_SIZE_SPEED( x ) ( ( (float)x / FLAME_LIFETIME ) / 1.0 ) // x is the current sizeMax #define GET_FLAME_SIZE_SPEED( x ) ( ( (float)x / FLAME_LIFETIME ) / 0.3 ) // x is the current sizeMax //#define FLAME_MIN_DRAWSIZE 20 // enable this for the fuel stream //#define FLAME_ENABLE_FUEL_STREAM // enable this for dynamic lighting around flames //#define FLAMETHROW_LIGHTS // disable this to stop rotating flames (this is variable so we can change it at run-time) int rotatingFlames = qtrue; /* =============== CG_FlameLerpVec =============== */ void CG_FlameLerpVec( const vec3_t oldV, const vec3_t newV, float backLerp, vec3_t outV ) { VectorScale( newV, ( 1.0 - backLerp ), outV ); VectorMA( outV, backLerp, oldV, outV ); } /* =============== CG_FlameAdjustSpeed =============== */ void CG_FlameAdjustSpeed( flameChunk_t *f, float change ) { if ( !f->velSpeed && !change ) { return; } f->velSpeed += change; if ( f->velSpeed < FLAME_MIN_SPEED ) { f->velSpeed = FLAME_MIN_SPEED; } } /* =============== CG_FireFlameChunks The given entity is firing a flamethrower =============== */ void CG_FireFlameChunks( centity_t *cent, vec3_t origin, vec3_t angles, float speedScale, qboolean firing ) { centFlameInfo_t *centInfo; flameChunk_t *f, *of; vec3_t lastFwd, thisFwd, fwd; vec3_t lastUp, thisUp, up; vec3_t lastRight, thisRight, right; vec3_t thisOrg, lastOrg, org; double timeInc, backLerp, fracInc; int t, numFrameChunks; double ft; trace_t trace; vec3_t parentFwd; //float frametime, dot; centInfo = ¢FlameInfo[cent->currentState.number]; // for any other character or in 3rd person view, use entity angles for friction if ( cent->currentState.number != cg.snap->ps.clientNum || cg_thirdPerson.integer ) { AngleVectors( cent->currentState.angles, parentFwd, NULL, NULL ); } else { AngleVectors( angles, parentFwd, NULL, NULL ); } AngleVectors( angles, thisFwd, thisRight, thisUp ); VectorCopy( origin, thisOrg ); // if this entity was firing last frame, interpolate the angles as we spawn the chunks that // fired over the last frame if ( ( centInfo->lastClientFrame == cent->currentState.frame ) && ( centInfo->lastFlameChunk && centInfo->lastFiring == firing ) ) { AngleVectors( centInfo->lastAngles, lastFwd, lastRight, lastUp ); VectorCopy( centInfo->lastOrigin, lastOrg ); centInfo->lastFiring = firing; of = centInfo->lastFlameChunk; timeInc = 1000.0 * ( firing ? 1.0 : 0.5 ) * ( FLAME_CHUNK_DIST / ( FLAME_START_SPEED * speedScale ) ); ft = ( (double)of->timeStart + timeInc ); t = (int)ft; fracInc = timeInc / (double)( cg.time - of->timeStart ); backLerp = 1.0 - fracInc; numFrameChunks = 0; // CHANGE: id while ( t <= cg.time ) { // spawn a new chunk CG_FlameLerpVec( lastOrg, thisOrg, backLerp, org ); CG_Trace( &trace, org, flameChunkMins, flameChunkMaxs, org, cent->currentState.number, MASK_SHOT | MASK_WATER ); // JPW NERVE water fixes if ( trace.startsolid ) { return; // don't spawn inside a wall } f = CG_SpawnFlameChunk( of ); if ( !f ) { //CG_Printf( "Out of flame chunks\n" ); // CHANGE: id // to make sure we do not keep trying to add more and more chunks centInfo->lastFlameChunk->timeStart = cg.time; // end CHANGE: id return; } CG_FlameLerpVec( lastFwd, thisFwd, backLerp, fwd ); VectorNormalize( fwd ); CG_FlameLerpVec( lastRight, thisRight, backLerp, right ); VectorNormalize( right ); CG_FlameLerpVec( lastUp, thisUp, backLerp, up ); VectorNormalize( up ); f->timeStart = t; f->timeEnd = t + FLAME_LIFETIME * ( 1.0 / ( 0.5 + 0.5 * speedScale ) ); f->size = FLAME_START_SIZE * speedScale; f->sizeMax = speedScale * ( FLAME_START_MAX_SIZE + f->sizeRand * ( firing ? 1.0 : 0.0 ) ); f->sizeRand = 0; if ( f->sizeMax > FLAME_MAX_SIZE ) { f->sizeMax = FLAME_MAX_SIZE; } f->sizeRate = GET_FLAME_BLUE_SIZE_SPEED( f->sizeMax * speedScale * ( 1.0 + ( 0.5 * (float)!firing ) ) ); VectorCopy( org, f->baseOrg ); f->baseOrgTime = t; VectorCopy( fwd, f->velDir ); VectorCopy( fwd, f->startVelDir ); f->speedScale = speedScale; VectorNormalize( f->velDir ); f->velSpeed = FLAME_START_SPEED * ( 0.5 + 0.5 * speedScale ) * ( firing ? 1.0 : 4.5 ); f->ownerCent = cent->currentState.number; f->rollAngle = crandom() * 179; f->ignitionOnly = !firing; if ( !firing ) { f->gravity = -150; f->blueLife = FLAME_BLUE_LIFE * 0.1; } else { f->gravity = 0; f->blueLife = FLAME_BLUE_LIFE; } f->lastFriction = cg.time; f->lastFrictionTake = cg.time; VectorCopy( parentFwd, f->parentFwd ); ft += timeInc; // always spawn a chunk right on the current time if ( (int)ft > cg.time && t < cg.time ) { ft = (double)cg.time; backLerp = fracInc; // so it'll get set to zero a few lines down } t = (int)ft; backLerp -= fracInc; centInfo->lastFlameChunk = of = f; // CHANGE: id // don't spawn too many chunks each frame if ( ++numFrameChunks > 50 ) { // to make sure we do not keep trying to add more and more chunks centInfo->lastFlameChunk->timeStart = cg.time; break; } // end CHANGE: id } } else { centInfo->lastFiring = firing; // just fire a single chunk to get us started f = CG_SpawnFlameChunk( NULL ); if ( !f ) { //CG_Printf( "Out of flame chunks\n" ); return; } VectorCopy( thisOrg, org ); VectorCopy( thisFwd, fwd ); VectorCopy( thisUp, up ); VectorCopy( thisRight, right ); f->timeStart = cg.time; f->timeEnd = cg.time + FLAME_LIFETIME * ( 1.0 / ( 0.5 + 0.5 * speedScale ) ); f->size = FLAME_START_SIZE * speedScale; f->sizeMax = FLAME_START_MAX_SIZE * speedScale; if ( f->sizeMax > FLAME_MAX_SIZE ) { f->sizeMax = FLAME_MAX_SIZE; } f->sizeRand = 0; f->sizeRate = GET_FLAME_BLUE_SIZE_SPEED( f->sizeMax * speedScale ); VectorCopy( org, f->baseOrg ); f->baseOrgTime = cg.time; VectorCopy( fwd, f->velDir ); VectorCopy( fwd, f->startVelDir ); f->velSpeed = FLAME_START_SPEED * ( 0.5 + 0.5 * speedScale ); f->ownerCent = cent->currentState.number; f->rollAngle = crandom() * 179; f->ignitionOnly = !firing; f->speedScale = speedScale; if ( !firing ) { f->gravity = -100; f->blueLife = (int)( 0.3 * ( 1.0 / speedScale ) * (float)FLAME_BLUE_LIFE ); } else { f->gravity = 0; f->blueLife = FLAME_BLUE_LIFE; } f->lastFriction = cg.time; f->lastFrictionTake = cg.time; VectorCopy( parentFwd, f->parentFwd ); centInfo->lastFlameChunk = f; } // push them along /* f = centInfo->lastFlameChunk; while (f) { if (f->lastFriction < cg.time - 50) { frametime = (float)(cg.time - f->lastFriction) / 1000.0; f->lastFriction = cg.time; dot = DotProduct(parentFwd, f->parentFwd); if (dot >= 0.99) { dot -= 0.99; dot *= (1.0/(1.0-0.99)); CG_FlameAdjustSpeed( f, 0.5 * frametime * FLAME_FRICTION_PER_SEC * pow(dot,4) ); } } f = f->nextFlameChunk; } */ VectorCopy( angles, centInfo->lastAngles ); VectorCopy( origin, centInfo->lastOrigin ); centInfo->lastClientFrame = cent->currentState.frame; } /* =============== CG_ClearFlameChunks =============== */ void CG_ClearFlameChunks( void ) { int i; memset( flameChunks, 0, sizeof( flameChunks ) ); memset( centFlameInfo, 0, sizeof( centFlameInfo ) ); freeFlameChunks = flameChunks; activeFlameChunks = NULL; headFlameChunks = NULL; for ( i = 0 ; i < MAX_FLAME_CHUNKS ; i++ ) { flameChunks[i].nextGlobal = &flameChunks[i + 1]; if ( i > 0 ) { flameChunks[i].prevGlobal = &flameChunks[i - 1]; } else { flameChunks[i].prevGlobal = NULL; } flameChunks[i].inuse = qfalse; } flameChunks[MAX_FLAME_CHUNKS - 1].nextGlobal = NULL; initFlameChunks = qtrue; numFlameChunksInuse = 0; } /* =============== CG_SpawnFlameChunk =============== */ flameChunk_t *CG_SpawnFlameChunk( flameChunk_t *headFlameChunk ) { flameChunk_t *f; if ( !freeFlameChunks ) { return NULL; } if ( headFlameChunks && headFlameChunks->dead ) { headFlameChunks = NULL; } // select the first free trail, and remove it from the list f = freeFlameChunks; freeFlameChunks = f->nextGlobal; if ( freeFlameChunks ) { freeFlameChunks->prevGlobal = NULL; } f->nextGlobal = activeFlameChunks; if ( activeFlameChunks ) { activeFlameChunks->prevGlobal = f; } activeFlameChunks = f; f->prevGlobal = NULL; f->inuse = qtrue; f->dead = qfalse; // if this owner has a headJunc, add us to the start if ( headFlameChunk ) { // remove the headJunc from the list of heads if ( headFlameChunk == headFlameChunks ) { headFlameChunks = headFlameChunks->nextHead; if ( headFlameChunks ) { headFlameChunks->prevHead = NULL; } } else { if ( headFlameChunk->nextHead ) { headFlameChunk->nextHead->prevHead = headFlameChunk->prevHead; } if ( headFlameChunk->prevHead ) { headFlameChunk->prevHead->nextHead = headFlameChunk->nextHead; } } headFlameChunk->prevHead = NULL; headFlameChunk->nextHead = NULL; } // make us the headTrail if ( headFlameChunks ) { headFlameChunks->prevHead = f; } f->nextHead = headFlameChunks; f->prevHead = NULL; headFlameChunks = f; f->nextFlameChunk = headFlameChunk; // if headJunc is NULL, then we'll just be the end of the list numFlameChunksInuse++; return f; } /* =========== CG_FreeFlameChunk =========== */ void CG_FreeFlameChunk( flameChunk_t *f ) { // kill any juncs after us, so they aren't left hanging if ( f->nextFlameChunk ) { CG_FreeFlameChunk( f->nextFlameChunk ); f->nextFlameChunk = NULL; } // make it non-active f->inuse = qfalse; f->dead = qfalse; if ( f->nextGlobal ) { f->nextGlobal->prevGlobal = f->prevGlobal; } if ( f->prevGlobal ) { f->prevGlobal->nextGlobal = f->nextGlobal; } if ( f == activeFlameChunks ) { activeFlameChunks = f->nextGlobal; } // if it's a head, remove it if ( f == headFlameChunks ) { headFlameChunks = f->nextHead; } if ( f->nextHead ) { f->nextHead->prevHead = f->prevHead; } if ( f->prevHead ) { f->prevHead->nextHead = f->nextHead; } f->nextHead = NULL; f->prevHead = NULL; // stick it in the free list f->prevGlobal = NULL; f->nextGlobal = freeFlameChunks; if ( freeFlameChunks ) { freeFlameChunks->prevGlobal = f; } freeFlameChunks = f; numFlameChunksInuse--; } /* =============== CG_MergeFlameChunks Assumes f1 comes before f2 =============== */ void CG_MergeFlameChunks( flameChunk_t *f1, flameChunk_t *f2 ) { if ( f1->nextFlameChunk != f2 ) { CG_Error( "CG_MergeFlameChunks: f2 doesn't follow f1, cannot merge\n" ); } f1->nextFlameChunk = f2->nextFlameChunk; f2->nextFlameChunk = NULL; VectorCopy( f2->velDir, f1->velDir ); VectorCopy( f2->baseOrg, f1->baseOrg ); f1->baseOrgTime = f2->baseOrgTime; f1->velSpeed = f2->velSpeed; f1->sizeMax = f2->sizeMax; f1->size = f2->size; f1->timeStart = f2->timeStart; f1->timeEnd = f2->timeEnd; CG_FreeFlameChunk( f2 ); } /* =============== CG_FlameCalcOrg =============== */ void CG_FlameCalcOrg( flameChunk_t *f, int time, vec3_t outOrg ) { VectorMA( f->baseOrg, f->velSpeed * ( (float)( time - f->baseOrgTime ) / 1000 ), f->velDir, outOrg ); //outOrg[2] -= f->gravity * ((float)(time - f->timeStart)/1000.0) * ((float)(time - f->timeStart)/1000.0); } /* =============== CG_MoveFlameChunk =============== */ void CG_MoveFlameChunk( flameChunk_t *f ) { vec3_t newOrigin, sOrg; trace_t trace; int jiggleCount; float dot; // TTimo: unused //static vec3_t umins = {-1,-1,-1}; //static vec3_t umaxs = { 1, 1, 1}; // subtract friction from speed if ( f->velSpeed > 1 && f->lastFrictionTake < cg.time - 50 ) { CG_FlameAdjustSpeed( f, -( (float)( cg.time - f->lastFrictionTake ) / 1000.0 ) * FLAME_FRICTION_PER_SEC ); f->lastFrictionTake = cg.time; } // adjust size if ( f->size < f->sizeMax ) { if ( ( cg.time - f->timeStart ) < f->blueLife ) { f->sizeRate = GET_FLAME_BLUE_SIZE_SPEED( FLAME_START_MAX_SIZE ); // use a constant so the blue flame doesn't distort } else { f->sizeRate = GET_FLAME_SIZE_SPEED( f->sizeMax ); } f->size += f->sizeRate * (float)( cg.time - f->baseOrgTime ); if ( f->size > f->sizeMax ) { f->size = f->sizeMax; } } jiggleCount = 0; VectorCopy( f->baseOrg, sOrg ); while ( f->velSpeed > 1 && f->baseOrgTime != cg.time ) { CG_FlameCalcOrg( f, cg.time, newOrigin ); // trace a line from previous position to new position CG_Trace( &trace, sOrg, flameChunkMins, flameChunkMaxs, newOrigin, f->ownerCent, MASK_SHOT | MASK_WATER ); // JPW NERVE water fixes if ( trace.startsolid ) { f->velSpeed = 0; f->dead = 1; // JPW NERVE water fixes break; } if ( trace.surfaceFlags & SURF_NOIMPACT ) { break; } // moved some distance VectorCopy( trace.endpos, f->baseOrg ); f->baseOrgTime += (int)( (float)( cg.time - f->baseOrgTime ) * trace.fraction ); if ( trace.fraction == 1.0 ) { // check for hitting client if ( ( f->ownerCent != cg.snap->ps.clientNum ) && !( cg.snap->ps.eFlags & EF_DEAD ) && VectorDistance( newOrigin, cg.snap->ps.origin ) < 32 ) { VectorNegate( f->velDir, trace.plane.normal ); } else { break; } } // reflect off surface dot = DotProduct( f->velDir, trace.plane.normal ); VectorMA( f->velDir, -2 * dot, trace.plane.normal, f->velDir ); VectorNormalize( f->velDir ); // subtract some speed f->velSpeed *= 0.5 * ( 0.25 + 0.75 * ( ( dot + 1.0 ) * 0.5 ) ); VectorCopy( f->velDir, f->parentFwd ); VectorCopy( f->baseOrg, sOrg ); } CG_FlameCalcOrg( f, cg.time, f->org ); f->baseOrgTime = cg.time; // incase we skipped the movement } /* =============== CG_AddFlameSpriteToScene =============== */ static vec3_t vright, vup; static vec3_t rright, rup; #ifdef _DEBUG // just in case we forget about it, but it should be disabled at all times (only enabled to generate updated shaders) #ifdef ALLOW_GEN_SHADERS // secondary security measure //#define GEN_FLAME_SHADER #endif // ALLOW_GEN_SHADERS #endif // _DEBUG #define FLAME_BLEND_SRC "GL_ONE" #define FLAME_BLEND_DST "GL_ONE_MINUS_SRC_COLOR" #define NUM_FLAME_SPRITES 45 #define FLAME_SPRITE_DIR "twiltb2" #define NUM_NOZZLE_SPRITES 8 static qhandle_t flameShaders[NUM_FLAME_SPRITES]; static qhandle_t nozzleShaders[NUM_NOZZLE_SPRITES]; static qboolean initFlameShaders = qtrue; #define MAX_CLIPPED_FLAMES 8 // dont draw more than this many per frame static int numClippedFlames; void CG_FlameDamage( int owner, vec3_t org, float radius ) { return; } void CG_AddFlameSpriteToScene( flameChunk_t *f, float lifeFrac, float alpha ) { vec3_t point, p2, sProj; float radius, sdist; int frameNum; vec3_t vec, rotate_ang; unsigned char alphaChar; vec2_t rST; static vec3_t lastPos; polyBuffer_t* pPolyBuffer; if ( alpha < 0 ) { return; // we dont want to see this } radius = ( f->size / 2.0 ); if ( radius < 6 ) { radius = 6; } if ( CG_CullPointAndRadius( f->org, radius ) ) { return; } rST[0] = radius * 1.0; rST[1] = radius * 1.0 / 1.481; alphaChar = ( unsigned char )( 255.0 * alpha ); frameNum = (int)floor( lifeFrac * NUM_FLAME_SPRITES ); if ( frameNum < 0 ) { frameNum = 0; } else if ( frameNum > NUM_FLAME_SPRITES - 1 ) { frameNum = NUM_FLAME_SPRITES - 1; } pPolyBuffer = CG_PB_FindFreePolyBuffer( cg_fxflags & 1 ? getTestShader() : flameShaders[frameNum], 4, 6 ); pPolyBuffer->color[pPolyBuffer->numVerts + 0][0] = alphaChar; pPolyBuffer->color[pPolyBuffer->numVerts + 0][1] = alphaChar; pPolyBuffer->color[pPolyBuffer->numVerts + 0][2] = alphaChar; pPolyBuffer->color[pPolyBuffer->numVerts + 0][3] = alphaChar; memcpy( pPolyBuffer->color[pPolyBuffer->numVerts + 1], pPolyBuffer->color[pPolyBuffer->numVerts + 0], sizeof( pPolyBuffer->color[0] ) ); memcpy( pPolyBuffer->color[pPolyBuffer->numVerts + 2], pPolyBuffer->color[pPolyBuffer->numVerts + 0], sizeof( pPolyBuffer->color[0] ) ); memcpy( pPolyBuffer->color[pPolyBuffer->numVerts + 3], pPolyBuffer->color[pPolyBuffer->numVerts + 0], sizeof( pPolyBuffer->color[0] ) ); // find the projected distance from the eye to the projection of the flame origin // onto the view direction vector VectorMA( cg.refdef_current->vieworg, 1024, cg.refdef_current->viewaxis[0], p2 ); ProjectPointOntoVector( f->org, cg.refdef_current->vieworg, p2, sProj ); // make sure its infront of us VectorSubtract( sProj, cg.refdef_current->vieworg, vec ); sdist = VectorNormalize( vec ); if ( !sdist || DotProduct( vec, cg.refdef_current->viewaxis[0] ) < 0 ) { return; } if ( ( rotatingFlames ) && ( !( cg_fxflags & 1 ) ) ) { // JPW NERVE no rotate for alt flame shaders vectoangles( cg.refdef_current->viewaxis[0], rotate_ang ); rotate_ang[ROLL] += f->rollAngle; AngleVectors( rotate_ang, NULL, rright, rup ); } else { VectorCopy( vright, rright ); VectorCopy( vup, rup ); } VectorMA( f->org, -rST[1], rup, point ); VectorMA( point, -rST[0], rright, point ); VectorCopy( point, pPolyBuffer->xyz[pPolyBuffer->numVerts + 0] ); pPolyBuffer->st[pPolyBuffer->numVerts + 0][0] = 0; pPolyBuffer->st[pPolyBuffer->numVerts + 0][1] = 0; VectorMA( point, rST[1] * 2, rup, point ); VectorCopy( point, pPolyBuffer->xyz[pPolyBuffer->numVerts + 1] ); pPolyBuffer->st[pPolyBuffer->numVerts + 1][0] = 0; pPolyBuffer->st[pPolyBuffer->numVerts + 1][1] = 1; VectorMA( point, rST[0] * 2, rright, point ); VectorCopy( point, pPolyBuffer->xyz[pPolyBuffer->numVerts + 2] ); pPolyBuffer->st[pPolyBuffer->numVerts + 2][0] = 1; pPolyBuffer->st[pPolyBuffer->numVerts + 2][1] = 1; VectorMA( point, -rST[1] * 2, rup, point ); VectorCopy( point, pPolyBuffer->xyz[pPolyBuffer->numVerts + 3] ); pPolyBuffer->st[pPolyBuffer->numVerts + 3][0] = 1; pPolyBuffer->st[pPolyBuffer->numVerts + 3][1] = 0; pPolyBuffer->indicies[pPolyBuffer->numIndicies + 0] = pPolyBuffer->numVerts + 0; pPolyBuffer->indicies[pPolyBuffer->numIndicies + 1] = pPolyBuffer->numVerts + 1; pPolyBuffer->indicies[pPolyBuffer->numIndicies + 2] = pPolyBuffer->numVerts + 2; pPolyBuffer->indicies[pPolyBuffer->numIndicies + 3] = pPolyBuffer->numVerts + 2; pPolyBuffer->indicies[pPolyBuffer->numIndicies + 4] = pPolyBuffer->numVerts + 3; pPolyBuffer->indicies[pPolyBuffer->numIndicies + 5] = pPolyBuffer->numVerts + 0; pPolyBuffer->numIndicies += 6; pPolyBuffer->numVerts += 4; VectorCopy( f->org, lastPos ); } static int nextFlameLight = 0; static int lastFlameOwner = -1; #define FLAME_SOUND_RANGE 1024.0 /* =============== CG_AddFlameToScene =============== */ void CG_AddFlameToScene( flameChunk_t *fHead ) { flameChunk_t *f, *fNext; int blueTrailHead = 0, fuelTrailHead = 0; static vec3_t whiteColor = {1,1,1}; vec3_t c; float alpha; float lived; int headTimeStart; float vdist, bdot; flameChunk_t *lastBlowChunk = NULL; qboolean isClientFlame, firing; int shader; flameChunk_t *lastBlueChunk = NULL; qboolean skip = qfalse, droppedTrail; vec3_t v; vec3_t lightOrg; // origin to place light at float lightSize; float lightFlameCount; float lastFuelAlpha; isClientFlame = ( fHead == centFlameInfo[fHead->ownerCent].lastFlameChunk ); if ( ( cg_entities[fHead->ownerCent].currentState.eFlags & EF_FIRING ) && ( centFlameInfo[fHead->ownerCent].lastFlameChunk == fHead ) ) { headTimeStart = fHead->timeStart; firing = qtrue; } else { headTimeStart = cg.time; firing = qfalse; } VectorClear( lightOrg ); lightSize = 0; lightFlameCount = 0; lastFuelAlpha = 1.0; f = fHead; while ( f ) { if ( f->nextFlameChunk && f->nextFlameChunk->dead ) { // kill it CG_FreeFlameChunk( f->nextFlameChunk ); f->nextFlameChunk = NULL; } // draw this chunk fNext = f->nextFlameChunk; lived = (float)( headTimeStart - f->timeStart ); // update the "blow" sound volume (louder as we sway it) vdist = Distance( cg.refdef_current->vieworg, f->org ); // NOTE: this needs to be here or the flameSound code further below won't work if ( lastBlowChunk && ( centFlameStatus[f->ownerCent].blowVolume < 1.0 ) && ( ( bdot = DotProduct( lastBlowChunk->startVelDir, f->startVelDir ) ) < 1.0 ) ) { if ( vdist < FLAME_SOUND_RANGE ) { centFlameStatus[f->ownerCent].blowVolume += 500.0 * ( 1.0 - bdot ) * ( 1.0 - ( vdist / FLAME_SOUND_RANGE ) ); if ( centFlameStatus[f->ownerCent].blowVolume > 1.0 ) { centFlameStatus[f->ownerCent].blowVolume = 1.0; } } } lastBlowChunk = f; VectorMA( lightOrg, f->size / 20.0, f->org, lightOrg ); lightSize += f->size; lightFlameCount += f->size / 20.0; droppedTrail = qfalse; // is it a stream chunk? (no special handling) if ( !f->ignitionOnly && f->velSpeed < 1 ) { CG_AddFlameSpriteToScene( f, f->lifeFrac, 1.0 ); // is it in the blue ignition section of the flame? } else if ( isClientFlame && f->blueLife > ( lived / 2.0 ) ) { skip = qfalse; // if this is backwards from the last chunk, then skip it if ( fNext && f != fHead && lastBlueChunk ) { VectorSubtract( f->org, lastBlueChunk->org, v ); if ( VectorNormalize( v ) < f->size / 2 ) { skip = qtrue; } else if ( DotProduct( v, f->velDir ) < 0 ) { skip = qtrue; } } // stream sound if ( !f->ignitionOnly ) { centFlameStatus[f->ownerCent].streamVolume += 0.05; if ( centFlameStatus[f->ownerCent].streamVolume > 1.0 ) { centFlameStatus[f->ownerCent].streamVolume = 1.0; } } if ( !skip ) { // just call this for damage checking //if (!f->ignitionOnly) //CG_AddFlameSpriteToScene( f, f->lifeFrac, -1 ); lastBlueChunk = f; alpha = 1.0; // new nozzle sprite VectorScale( whiteColor, alpha, c ); if ( f->blueLife > lived * ( f->ignitionOnly ? 3.0 : 3.0 ) ) { shader = nozzleShaders[( cg.time / 50 + ( cg.time / 50 >> 1 ) ) % NUM_NOZZLE_SPRITES]; blueTrailHead = CG_AddTrailJunc( blueTrailHead, NULL, // rain - zinx's trail fix shader, cg.time, STYPE_STRETCH, f->org, 1, alpha, alpha, f->size * ( f->ignitionOnly /*&& (cg.snap->ps.clientNum != f->ownerCent || cg_thirdPerson.integer)*/ ? 2.0 : 1.0 ), FLAME_MAX_SIZE, TJFL_NOCULL | TJFL_FIXDISTORT, c, c, 1.0, 5.0 ); } // fire stream if ( !f->ignitionOnly ) { float bscale; qboolean fskip = qfalse; bscale = 1.0; if ( !f->nextFlameChunk ) { alpha = 0; } else if ( lived / 1.3 < bscale * FLAME_BLUE_FADEIN_TIME( f->blueLife ) ) { alpha = FLAME_BLUE_MAX_ALPHA * ( ( lived / 1.3 ) / ( bscale * FLAME_BLUE_FADEIN_TIME( f->blueLife ) ) ); } else if ( lived / 1.3 < ( f->blueLife - FLAME_BLUE_FADEOUT_TIME( f->blueLife ) ) ) { alpha = FLAME_BLUE_MAX_ALPHA; } else { alpha = FLAME_BLUE_MAX_ALPHA * ( 1.0 - ( ( lived / 1.3 - ( f->blueLife - FLAME_BLUE_FADEOUT_TIME( f->blueLife ) ) ) / ( FLAME_BLUE_FADEOUT_TIME( f->blueLife ) ) ) ); } if ( alpha <= 0.0 ) { alpha = 0.0; if ( lastFuelAlpha <= 0.0 ) { fskip = qtrue; } } if ( !fskip ) { lastFuelAlpha = alpha; VectorScale( whiteColor, alpha, c ); droppedTrail = qtrue; fuelTrailHead = CG_AddTrailJunc( fuelTrailHead, NULL, // rain - zinx's trail fix cgs.media.flamethrowerFireStream, cg.time, ( f->ignitionOnly ? STYPE_STRETCH : STYPE_REPEAT ), f->org, 1, alpha, alpha, ( f->size / 2 < f->sizeMax / 4 ? f->size / 2 : f->sizeMax / 4 ), FLAME_MAX_SIZE, TJFL_NOCULL | TJFL_FIXDISTORT | TJFL_CROSSOVER, c, c, 0.5, 1.5 ); } } } } #define FLAME_SPRITE_START_BLUE_SCALE 0.2 if ( !f->ignitionOnly && ( (float)( FLAME_SPRITE_START_BLUE_SCALE * f->blueLife ) < (float)lived ) ) { float alpha, lifeFrac; qboolean skip = qfalse; // should we merge it with the next sprite? while ( fNext && !droppedTrail ) { if ( ( Distance( f->org, fNext->org ) < ( ( 0.1 + 0.9 * f->lifeFrac ) * f->size * 0.35 ) ) && ( fabs( f->size - fNext->size ) < ( 40.0 ) ) && ( fabs( f->timeStart - fNext->timeStart ) < 100 ) && ( DotProduct( f->velDir, fNext->velDir ) > 0.99 ) ) { if ( !droppedTrail ) { CG_MergeFlameChunks( f, fNext ); fNext = f->nextFlameChunk; // it may have changed } else { skip = qtrue; break; } } else { break; } } lifeFrac = ( lived - FLAME_SPRITE_START_BLUE_SCALE * f->blueLife ) / ( FLAME_LIFETIME - FLAME_SPRITE_START_BLUE_SCALE * f->blueLife ); alpha = ( 1.0 - lifeFrac ) * 1.4; if ( alpha > 1.0 ) { alpha = 1.0; } if ( !skip ) { // draw the sprite CG_AddFlameSpriteToScene( f, lifeFrac, alpha ); } // update the sizeRate f->sizeRate = GET_FLAME_SIZE_SPEED( f->sizeMax ); } f = fNext; } if ( lastFlameOwner == fHead->ownerCent && nextFlameLight == cg.clientFrame ) { return; } if ( !fHead->ignitionOnly ) { nextFlameLight = cg.clientFrame; lastFlameOwner = fHead->ownerCent; } if ( lightSize < 80 ) { lightSize = 80; } if ( lightSize > 500 ) { lightSize = 500; } lightSize *= 1.0 + 0.2 * ( sin( 1.0 * cg.time / 50.0 ) * cos( 1.0 * cg.time / 43.0 ) ); // set the alpha //% alpha = lightSize / 500.0; alpha = lightSize * 0.005; // ydnar if ( alpha > 2.0 ) { alpha = 2.0; } VectorScale( lightOrg, 1.0 / lightFlameCount, lightOrg ); // if it's only a nozzle, make it blue if ( fHead->ignitionOnly ) { if ( lightSize > 80 ) { lightSize = 80; } //% trap_R_AddLightToScene( lightOrg, 90 + lightSize, 0, 0, alpha, 0 + isClientFlame * (fHead->ownerCent == cg.snap->ps.clientNum) ); trap_R_AddLightToScene( lightOrg, 80, alpha, 0.2, 0.21, 0.5, 0, 0 ); } else if ( isClientFlame || ( fHead->ownerCent == cg.snap->ps.clientNum ) ) { //% trap_R_AddLightToScene( lightOrg, 90 + lightSize, 1.000000*alpha, 0.603922*alpha, 0.207843*alpha, 0 ); trap_R_AddLightToScene( lightOrg, 320, alpha, 1.000000, 0.603922, 0.207843, 0, 0 ); } } /* ============= CG_GenerateShaders A util to create a bunch of shaders in a unique shader file, which represent an animation ============= */ void CG_GenerateShaders( char *filename, char *shaderName, char *dir, int numFrames, char *srcBlend, char *dstBlend, char *extras, qboolean compressedVersionAvailable, qboolean nomipmap ) { fileHandle_t f; int b, c, d, lastNumber; char str[512]; int i; trap_FS_FOpenFile( filename, &f, FS_WRITE ); for ( i = 0; i < numFrames; i++ ) { lastNumber = i; b = lastNumber / 100; lastNumber -= b * 100; c = lastNumber / 10; lastNumber -= c * 10; d = lastNumber; if ( compressedVersionAvailable ) { Com_sprintf( str, sizeof( str ), "%s%i\n{\n\tnofog%s\n\tallowCompress\n\tcull none\n\t{\n\t\tmapcomp sprites/%s_lg/spr%i%i%i.tga\n\t\tmapnocomp sprites/%s/spr%i%i%i.tga\n\t\tblendFunc %s %s\n%s\t}\n}\n", shaderName, i + 1, nomipmap ? "\n\tnomipmaps" : "", dir, b, c, d, dir, b, c, d, srcBlend, dstBlend, extras ); } else { Com_sprintf( str, sizeof( str ), "%s%i\n{\n\tnofog%s\n\tallowCompress\n\tcull none\n\t{\n\t\tmap sprites/%s/spr%i%i%i.tga\n\t\tblendFunc %s %s\n%s\t}\n}\n", shaderName, i + 1, nomipmap ? "\n\tnomipmap" : "", dir, b, c, d, srcBlend, dstBlend, extras ); } trap_FS_Write( str, strlen( str ), f ); } trap_FS_FCloseFile( f ); } /* =============== CG_InitFlameChunks =============== */ void CG_InitFlameChunks( void ) { int i; char filename[MAX_QPATH]; CG_ClearFlameChunks(); #ifdef GEN_FLAME_SHADER CG_GenerateShaders( "scripts/flamethrower.shader", "flamethrowerFire", FLAME_SPRITE_DIR, NUM_FLAME_SPRITES, FLAME_BLEND_SRC, FLAME_BLEND_DST, "", qtrue, qtrue ); CG_GenerateShaders( "scripts/blacksmokeanim.shader", "blacksmokeanim", "explode1", 23, "GL_ZERO", "GL_ONE_MINUS_SRC_ALPHA", "\t\talphaGen const 0.2\n", qfalse, qfalse ); CG_GenerateShaders( "scripts/viewflames.shader", "viewFlashFire", "clnfire", 16, "GL_ONE", "GL_ONE", "\t\talphaGen vertex\n\t\trgbGen vertex\n", qtrue, qtrue ); CG_GenerateShaders( "scripts/twiltb.shader", "twiltb", "twiltb", 42, "GL_SRC_ALPHA", "GL_ONE_MINUS_SRC_COLOR", "", qtrue, qfalse ); CG_GenerateShaders( "scripts/twiltb2.shader", "twiltb2", "twiltb2", 45, "GL_ONE", "GL_ONE_MINUS_SRC_COLOR", "", qtrue, qfalse ); /* CG_GenerateShaders( "scripts/expblue.shader", "expblue", "expblue", 25, "GL_ONE", "GL_ONE_MINUS_SRC_COLOR", "", qfalse, qfalse ); */ CG_GenerateShaders( "scripts/firest.shader", "firest", "firest", 36, "GL_ONE", "GL_ONE_MINUS_SRC_COLOR", "", qtrue, qfalse ); CG_GenerateShaders( "scripts/explode1.shader", "explode1", "explode1", 23, "GL_ONE", "GL_ONE_MINUS_SRC_COLOR", "", qtrue, qfalse ); CG_GenerateShaders( "scripts/funnel.shader", "funnel", "funnel", 21, "GL_ONE", "GL_ONE_MINUS_SRC_COLOR", "", qfalse, qfalse ); #endif for ( i = 0; i < NUM_FLAME_SPRITES; i++ ) { Com_sprintf( filename, MAX_QPATH, "flamethrowerFire%i", i + 1 ); flameShaders[i] = trap_R_RegisterShader( filename ); } for ( i = 0; i < NUM_NOZZLE_SPRITES; i++ ) { Com_sprintf( filename, MAX_QPATH, "nozzleFlame%i", i + 1 ); nozzleShaders[i] = trap_R_RegisterShader( filename ); } initFlameShaders = qfalse; } /* =============== CG_AddFlameChunks =============== */ void CG_AddFlameChunks( void ) { flameChunk_t *f, *fNext; //AngleVectors( cg.refdef.viewangles, NULL, vright, vup ); VectorCopy( cg.refdef_current->viewaxis[1], vright ); VectorCopy( cg.refdef_current->viewaxis[2], vup ); // clear out the volumes so we can rebuild them memset( centFlameStatus, 0, sizeof( centFlameStatus ) ); numClippedFlames = 0; // age them f = activeFlameChunks; while ( f ) { if ( !f->dead ) { if ( cg.time > f->timeEnd ) { f->dead = qtrue; } else if ( f->ignitionOnly && ( f->blueLife < ( cg.time - f->timeStart ) ) ) { f->dead = qtrue; } else { CG_MoveFlameChunk( f ); f->lifeFrac = (float)( cg.time - f->timeStart ) / (float)( f->timeEnd - f->timeStart ); } } f = f->nextGlobal; } // draw each of the headFlameChunk's f = headFlameChunks; while ( f ) { fNext = f->nextHead; // in case it gets removed if ( f->dead ) { if ( centFlameInfo[f->ownerCent].lastFlameChunk == f ) { centFlameInfo[f->ownerCent].lastFlameChunk = NULL; centFlameInfo[f->ownerCent].lastClientFrame = 0; } CG_FreeFlameChunk( f ); } else if ( !f->ignitionOnly || ( centFlameInfo[f->ownerCent].lastFlameChunk == f ) ) { // don't draw the ignition flame after we start firing CG_AddFlameToScene( f ); } f = fNext; } } /* =============== CG_UpdateFlamethrowerSounds =============== */ void CG_UpdateFlamethrowerSounds( void ) { flameChunk_t *f, *trav; // , *lastSoundFlameChunk=NULL; // TTimo: unused #define MIN_BLOW_VOLUME 30 // draw each of the headFlameChunk's f = headFlameChunks; while ( f ) { // update this entity? if ( centFlameInfo[f->ownerCent].lastSoundUpdate != cg.time ) { // blow/ignition sound if ( centFlameStatus[f->ownerCent].blowVolume * 255.0 > MIN_BLOW_VOLUME ) { trap_S_AddLoopingSound( f->org, vec3_origin, cgs.media.flameBlowSound, (int)( 255.0 * centFlameStatus[f->ownerCent].blowVolume ), 0 ); // JPW NERVE } else { trap_S_AddLoopingSound( f->org, vec3_origin, cgs.media.flameBlowSound, MIN_BLOW_VOLUME, 0 ); // JPW NERVE } if ( centFlameStatus[f->ownerCent].streamVolume ) { trap_S_AddLoopingSound( f->org, vec3_origin, cgs.media.flameStreamSound, (int)( 255.0 * centFlameStatus[f->ownerCent].streamVolume ), 0 ); // JPW NERVE } centFlameInfo[f->ownerCent].lastSoundUpdate = cg.time; } // traverse the chunks, spawning flame sound sources as we go for ( trav = f; trav; trav = trav->nextFlameChunk ) { // update the sound volume if ( trav->blueLife + 100 < ( cg.time - trav->timeStart ) ) { trap_S_AddLoopingSound( trav->org, vec3_origin, cgs.media.flameSound, (int)( 255.0 * ( 0.2 * ( trav->size / FLAME_MAX_SIZE ) ) ), 0 ); } } f = f->nextHead; } }