+ Cvar_RegisterVariable (&cl_decals);
+ Cvar_RegisterVariable (&cl_decals_time);
+ Cvar_RegisterVariable (&cl_decals_fadetime);
+}
+
+void CL_Particles_Shutdown (void)
+{
+}
+
+// list of all 26 parameters:
+// ptype - any of the pt_ enum values (pt_static, pt_blood, etc), see ptype_t near the top of this file
+// pcolor1,pcolor2 - minimum and maximum ranges of color, randomly interpolated to decide particle color
+// ptex - any of the tex_ values such as tex_smoke[rand()&7] or tex_particle
+// psize - size of particle (or thickness for PARTICLE_SPARK and PARTICLE_BEAM)
+// palpha - opacity of particle as 0-255 (can be more than 255)
+// palphafade - rate of fade per second (so 256 would mean a 256 alpha particle would fade to nothing in 1 second)
+// ptime - how long the particle can live (note it is also removed if alpha drops to nothing)
+// pgravity - how much effect gravity has on the particle (0-1)
+// pbounce - how much bounce the particle has when it hits a surface (0-1), -1 makes a blood splat when it hits a surface, 0 does not even check for collisions
+// px,py,pz - starting origin of particle
+// pvx,pvy,pvz - starting velocity of particle
+// pfriction - how much the particle slows down per second (0-1 typically, can slowdown faster than 1)
+static particle_t *particle(particletype_t *ptype, int pcolor1, int pcolor2, int ptex, float psize, float psizeincrease, float palpha, float palphafade, float pgravity, float pbounce, float px, float py, float pz, float pvx, float pvy, float pvz, float pairfriction, float pliquidfriction, float originjitter, float velocityjitter)
+{
+ int l1, l2;
+ particle_t *part;
+ vec3_t v;
+ for (;cl.free_particle < cl.max_particles && cl.particles[cl.free_particle].type;cl.free_particle++);
+ if (cl.free_particle >= cl.max_particles)
+ return NULL;
+ part = &cl.particles[cl.free_particle++];
+ if (cl.num_particles < cl.free_particle)
+ cl.num_particles = cl.free_particle;
+ memset(part, 0, sizeof(*part));
+ part->type = ptype;
+ l2 = (int)lhrandom(0.5, 256.5);
+ l1 = 256 - l2;
+ part->color[0] = ((((pcolor1 >> 16) & 0xFF) * l1 + ((pcolor2 >> 16) & 0xFF) * l2) >> 8) & 0xFF;
+ part->color[1] = ((((pcolor1 >> 8) & 0xFF) * l1 + ((pcolor2 >> 8) & 0xFF) * l2) >> 8) & 0xFF;
+ part->color[2] = ((((pcolor1 >> 0) & 0xFF) * l1 + ((pcolor2 >> 0) & 0xFF) * l2) >> 8) & 0xFF;
+ part->color[3] = 0xFF;
+ part->texnum = ptex;
+ part->size = psize;
+ part->sizeincrease = psizeincrease;
+ part->alpha = palpha;
+ part->alphafade = palphafade;
+ part->gravity = pgravity;
+ part->bounce = pbounce;
+ VectorRandom(v);
+ part->org[0] = px + originjitter * v[0];
+ part->org[1] = py + originjitter * v[1];
+ part->org[2] = pz + originjitter * v[2];
+ part->vel[0] = pvx + velocityjitter * v[0];
+ part->vel[1] = pvy + velocityjitter * v[1];
+ part->vel[2] = pvz + velocityjitter * v[2];
+ part->time2 = 0;
+ part->airfriction = pairfriction;
+ part->liquidfriction = pliquidfriction;
+ part->die = cl.time + part->alpha / (part->alphafade ? part->alphafade : 1);
+ part->delayedcollisions = 0;
+ // if it is rain or snow, trace ahead and shut off collisions until an actual collision event needs to occur to improve performance
+ if (part->type == particletype + pt_rain)
+ {
+ int i;
+ particle_t *part2;
+ float lifetime = part->die - cl.time;
+ vec3_t endvec;
+ trace_t trace;
+ // turn raindrop into simple spark and create delayedspawn splash effect
+ part->type = particletype + pt_spark;
+ part->bounce = 0;
+ VectorMA(part->org, lifetime, part->vel, endvec);
+ trace = CL_Move(part->org, vec3_origin, vec3_origin, endvec, MOVE_NOMONSTERS, NULL, SUPERCONTENTS_SOLID | SUPERCONTENTS_BODY | ((part->type == particletype + pt_rain || part->type == particletype + pt_snow) ? SUPERCONTENTS_LIQUIDSMASK : 0), true, false, NULL, false);
+ part->die = cl.time + lifetime * trace.fraction;
+ part2 = particle(particletype + pt_raindecal, pcolor1, pcolor2, tex_rainsplash, part->size, part->size * 20, part->alpha, part->alpha / 0.4, 0, 0, trace.endpos[0] + trace.plane.normal[0], trace.endpos[1] + trace.plane.normal[1], trace.endpos[2] + trace.plane.normal[2], trace.plane.normal[0], trace.plane.normal[1], trace.plane.normal[2], 0, 0, 0, 0);
+ if (part2)
+ {
+ part2->delayedspawn = part->die;
+ part2->die += part->die - cl.time;
+ for (i = rand() & 7;i < 10;i++)
+ {
+ part2 = particle(particletype + pt_spark, pcolor1, pcolor2, tex_particle, 0.25f, 0, part->alpha * 2, part->alpha * 4, 1, 0, trace.endpos[0] + trace.plane.normal[0], trace.endpos[1] + trace.plane.normal[1], trace.endpos[2] + trace.plane.normal[2], trace.plane.normal[0] * 16, trace.plane.normal[1] * 16, trace.plane.normal[2] * 16 + cl.movevars_gravity * 0.04, 0, 0, 0, 32);
+ if (part2)
+ {
+ part2->delayedspawn = part->die;
+ part2->die += part->die - cl.time;
+ }
+ }
+ }
+ }
+ else if (part->bounce != 0 && part->gravity == 0)
+ {
+ float lifetime = part->alpha / (part->alphafade ? part->alphafade : 1);
+ vec3_t endvec;
+ trace_t trace;
+ VectorMA(part->org, lifetime, part->vel, endvec);
+ trace = CL_Move(part->org, vec3_origin, vec3_origin, endvec, MOVE_NOMONSTERS, NULL, SUPERCONTENTS_SOLID | SUPERCONTENTS_BODY | ((part->type == particletype + pt_rain || part->type == particletype + pt_snow) ? SUPERCONTENTS_LIQUIDSMASK : 0), true, false, NULL, false);
+ part->delayedcollisions = cl.time + lifetime * trace.fraction;
+ }
+ return part;
+}
+
+void CL_SpawnDecalParticleForSurface(int hitent, const vec3_t org, const vec3_t normal, int color1, int color2, int texnum, float size, float alpha)
+{
+ particle_t *p;
+ if (!cl_decals.integer)
+ return;
+ p = particle(particletype + pt_decal, color1, color2, texnum, size, 0, alpha, 0, 0, 0, org[0] + normal[0], org[1] + normal[1], org[2] + normal[2], normal[0], normal[1], normal[2], 0, 0, 0, 0);
+ if (p)
+ {
+ p->time2 = cl.time;
+ p->owner = hitent;
+ p->ownermodel = cl.entities[p->owner].render.model;
+ VectorAdd(org, normal, p->org);
+ VectorCopy(normal, p->vel);
+ // these relative things are only used to regenerate p->org and p->vel if p->owner is not world (0)
+ Matrix4x4_Transform(&cl.entities[p->owner].render.inversematrix, org, p->relativeorigin);
+ Matrix4x4_Transform3x3(&cl.entities[p->owner].render.inversematrix, normal, p->relativedirection);
+ }
+}
+
+void CL_SpawnDecalParticleForPoint(const vec3_t org, float maxdist, float size, float alpha, int texnum, int color1, int color2)
+{
+ int i;
+ float bestfrac, bestorg[3], bestnormal[3];
+ float org2[3];
+ int besthitent = 0, hitent;
+ trace_t trace;
+ bestfrac = 10;
+ for (i = 0;i < 32;i++)
+ {
+ VectorRandom(org2);
+ VectorMA(org, maxdist, org2, org2);
+ trace = CL_Move(org, vec3_origin, vec3_origin, org2, MOVE_NOMONSTERS, NULL, SUPERCONTENTS_SOLID | SUPERCONTENTS_SKY, true, false, &hitent, false);
+ // take the closest trace result that doesn't end up hitting a NOMARKS
+ // surface (sky for example)
+ if (bestfrac > trace.fraction && !(trace.hitq3surfaceflags & Q3SURFACEFLAG_NOMARKS))
+ {
+ bestfrac = trace.fraction;
+ besthitent = hitent;
+ VectorCopy(trace.endpos, bestorg);
+ VectorCopy(trace.plane.normal, bestnormal);
+ }
+ }
+ if (bestfrac < 1)
+ CL_SpawnDecalParticleForSurface(besthitent, bestorg, bestnormal, color1, color2, texnum, size, alpha);
+}
+
+static void CL_Sparks(const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, float sparkcount);
+static void CL_Smoke(const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, float smokecount);
+void CL_ParticleEffect_Fallback(int effectnameindex, float count, const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, entity_t *ent, int palettecolor, qboolean spawndlight, qboolean spawnparticles)
+{
+ vec3_t center;
+ matrix4x4_t tempmatrix;
+ VectorLerp(originmins, 0.5, originmaxs, center);
+ Matrix4x4_CreateTranslate(&tempmatrix, center[0], center[1], center[2]);
+ if (effectnameindex == EFFECT_SVC_PARTICLE)
+ {
+ if (cl_particles.integer)
+ {
+ // bloodhack checks if this effect's color matches regular or lightning blood and if so spawns a blood effect instead
+ if (count == 1024)
+ CL_ParticleExplosion(center);
+ else if (cl_particles_blood_bloodhack.integer && !cl_particles_quake.integer && (palettecolor == 73 || palettecolor == 225))
+ CL_ParticleEffect(EFFECT_TE_BLOOD, count / 6.0f, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+ else
+ {
+ count *= cl_particles_quality.value;
+ for (;count > 0;count--)
+ {
+ int k = particlepalette[palettecolor + (rand()&7)];
+ if (cl_particles_quake.integer)
+ particle(particletype + pt_alphastatic, k, k, tex_particle, 1.5, 0, lhrandom(51, 255), 512, 0.05, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 8, 0);
+ else if (gamemode == GAME_GOODVSBAD2)
+ particle(particletype + pt_alphastatic, k, k, tex_particle, 5, 0, 255, 300, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 8, 10);
+ else
+ particle(particletype + pt_alphastatic, k, k, tex_particle, 1.5, 0, 255, 512, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 8, 15);
+ }
+ }
+ }
+ }
+ else if (effectnameindex == EFFECT_TE_WIZSPIKE)
+ CL_ParticleEffect(EFFECT_SVC_PARTICLE, 30*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 20);
+ else if (effectnameindex == EFFECT_TE_KNIGHTSPIKE)
+ CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 226);
+ else if (effectnameindex == EFFECT_TE_SPIKE)
+ {
+ if (cl_particles_bulletimpacts.integer)
+ {
+ if (cl_particles_quake.integer)
+ {
+ if (cl_particles_smoke.integer)
+ CL_ParticleEffect(EFFECT_SVC_PARTICLE, 10*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+ }
+ else
+ {
+ CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 4*count);
+ CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 15*count);
+ }
+ }
+ // bullet hole
+ if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+ CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+ }
+ else if (effectnameindex == EFFECT_TE_SPIKEQUAD)
+ {
+ if (cl_particles_bulletimpacts.integer)
+ {
+ if (cl_particles_quake.integer)
+ {
+ if (cl_particles_smoke.integer)
+ CL_ParticleEffect(EFFECT_SVC_PARTICLE, 10*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+ }
+ else
+ {
+ CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 4*count);
+ CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 15*count);
+ }
+ }
+ // bullet hole
+ if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+ CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+ CL_AllocLightFlash(NULL, &tempmatrix, 100, 0.15f, 0.15f, 1.5f, 500, 0.2, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_TE_SUPERSPIKE)
+ {
+ if (cl_particles_bulletimpacts.integer)
+ {
+ if (cl_particles_quake.integer)
+ {
+ if (cl_particles_smoke.integer)
+ CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+ }
+ else
+ {
+ CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 8*count);
+ CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 30*count);
+ }
+ }
+ // bullet hole
+ if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+ CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+ }
+ else if (effectnameindex == EFFECT_TE_SUPERSPIKEQUAD)
+ {
+ if (cl_particles_bulletimpacts.integer)
+ {
+ if (cl_particles_quake.integer)
+ {
+ if (cl_particles_smoke.integer)
+ CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+ }
+ else
+ {
+ CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 8*count);
+ CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 30*count);
+ }
+ }
+ // bullet hole
+ if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+ CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+ CL_AllocLightFlash(NULL, &tempmatrix, 100, 0.15f, 0.15f, 1.5f, 500, 0.2, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_TE_BLOOD)
+ {
+ if (!cl_particles_blood.integer)
+ return;
+ if (cl_particles_quake.integer)
+ CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 73);
+ else
+ {
+ static double bloodaccumulator = 0;
+ bloodaccumulator += count * 0.333 * cl_particles_quality.value;
+ for (;bloodaccumulator > 0;bloodaccumulator--)
+ particle(particletype + pt_blood, 0xFFFFFF, 0xFFFFFF, tex_bloodparticle[rand()&7], 8, 0, cl_particles_blood_alpha.value * 768, cl_particles_blood_alpha.value * 384, 0, -1, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 0, 64);
+ }
+ }
+ else if (effectnameindex == EFFECT_TE_SPARK)
+ CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, count);
+ else if (effectnameindex == EFFECT_TE_PLASMABURN)
+ {
+ // plasma scorch mark
+ if (cl_stainmaps.integer) R_Stain(center, 48, 96, 96, 96, 32, 128, 128, 128, 32);
+ CL_SpawnDecalParticleForPoint(center, 6, 6, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+ CL_AllocLightFlash(NULL, &tempmatrix, 200, 1, 1, 1, 1000, 0.2, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_TE_GUNSHOT)
+ {
+ if (cl_particles_bulletimpacts.integer)
+ {
+ if (cl_particles_quake.integer)
+ CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+ else
+ {
+ CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 4*count);
+ CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 20*count);
+ }
+ }
+ // bullet hole
+ if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+ CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+ }
+ else if (effectnameindex == EFFECT_TE_GUNSHOTQUAD)
+ {
+ if (cl_particles_bulletimpacts.integer)
+ {
+ if (cl_particles_quake.integer)
+ CL_ParticleEffect(EFFECT_SVC_PARTICLE, 20*count, originmins, originmaxs, velocitymins, velocitymaxs, NULL, 0);
+ else
+ {
+ CL_Smoke(originmins, originmaxs, velocitymins, velocitymaxs, 4*count);
+ CL_Sparks(originmins, originmaxs, velocitymins, velocitymaxs, 20*count);
+ }
+ }
+ // bullet hole
+ if (cl_stainmaps.integer) R_Stain(center, 32, 96, 96, 96, 24, 128, 128, 128, 24);
+ CL_SpawnDecalParticleForPoint(center, 6, 3, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+ CL_AllocLightFlash(NULL, &tempmatrix, 100, 0.15f, 0.15f, 1.5f, 500, 0.2, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_TE_EXPLOSION)
+ {
+ CL_ParticleExplosion(center);
+ CL_AllocLightFlash(NULL, &tempmatrix, 350, 4.0f, 2.0f, 0.50f, 700, 0.5, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_TE_EXPLOSIONQUAD)
+ {
+ CL_ParticleExplosion(center);
+ CL_AllocLightFlash(NULL, &tempmatrix, 350, 2.5f, 2.0f, 4.0f, 700, 0.5, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_TE_TAREXPLOSION)
+ {
+ if (cl_particles_quake.integer)
+ {
+ int i;
+ for (i = 0;i < 1024 * cl_particles_quality.value;i++)
+ {
+ if (i & 1)
+ particle(particletype + pt_static, particlepalette[66], particlepalette[71], tex_particle, 1.5f, 0, lhrandom(182, 255), 182, 0, 0, center[0], center[1], center[2], 0, 0, 0, -4, -4, 16, 256);
+ else
+ particle(particletype + pt_static, particlepalette[150], particlepalette[155], tex_particle, 1.5f, 0, lhrandom(182, 255), 182, 0, 0, center[0], center[1], center[2], 0, 0, lhrandom(-256, 256), 0, 0, 16, 0);
+ }
+ }
+ else
+ CL_ParticleExplosion(center);
+ CL_AllocLightFlash(NULL, &tempmatrix, 600, 1.6f, 0.8f, 2.0f, 1200, 0.5, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_TE_SMALLFLASH)
+ CL_AllocLightFlash(NULL, &tempmatrix, 200, 2, 2, 2, 1000, 0.2, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ else if (effectnameindex == EFFECT_TE_FLAMEJET)
+ {
+ count *= cl_particles_quality.value;
+ while (count-- > 0)
+ particle(particletype + pt_smoke, 0x6f0f00, 0xe3974f, tex_particle, 4, 0, lhrandom(64, 128), 384, -1, 1.1, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 0, 128);
+ }
+ else if (effectnameindex == EFFECT_TE_LAVASPLASH)
+ {
+ float i, j, inc, vel;
+ vec3_t dir, org;
+
+ inc = 8 / cl_particles_quality.value;
+ for (i = -128;i < 128;i += inc)
+ {
+ for (j = -128;j < 128;j += inc)
+ {
+ dir[0] = j + lhrandom(0, inc);
+ dir[1] = i + lhrandom(0, inc);
+ dir[2] = 256;
+ org[0] = center[0] + dir[0];
+ org[1] = center[1] + dir[1];
+ org[2] = center[2] + lhrandom(0, 64);
+ vel = lhrandom(50, 120) / VectorLength(dir); // normalize and scale
+ particle(particletype + pt_alphastatic, particlepalette[224], particlepalette[231], tex_particle, 1.5f, 0, inc * lhrandom(24, 32), inc * 12, 0.05, 0, org[0], org[1], org[2], dir[0] * vel, dir[1] * vel, dir[2] * vel, 0, 0, 0, 0);
+ }
+ }
+ }
+ else if (effectnameindex == EFFECT_TE_TELEPORT)
+ {
+ float i, j, k, inc, vel;
+ vec3_t dir;
+
+ inc = 8 / cl_particles_quality.value;
+ for (i = -16;i < 16;i += inc)
+ {
+ for (j = -16;j < 16;j += inc)
+ {
+ for (k = -24;k < 32;k += inc)
+ {
+ VectorSet(dir, i*8, j*8, k*8);
+ VectorNormalize(dir);
+ vel = lhrandom(50, 113);
+ particle(particletype + pt_alphastatic, particlepalette[7], particlepalette[14], tex_particle, 1.5f, 0, inc * lhrandom(37, 63), inc * 187, 0, 0, center[0] + i + lhrandom(0, inc), center[1] + j + lhrandom(0, inc), center[2] + k + lhrandom(0, inc), dir[0] * vel, dir[1] * vel, dir[2] * vel, 0, 0, 0, 0);
+ }
+ }
+ }
+ particle(particletype + pt_static, particlepalette[14], particlepalette[14], tex_particle, 30, 0, 256, 512, 0, 0, center[0], center[1], center[2], 0, 0, 0, 0, 0, 0, 0);
+ CL_AllocLightFlash(NULL, &tempmatrix, 200, 2.0f, 2.0f, 2.0f, 400, 99.0f, 0, -1, true, 1, 0.25, 1, 0, 0, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_TE_TEI_G3)
+ particle(particletype + pt_beam, 0xFFFFFF, 0xFFFFFF, tex_beam, 8, 0, 256, 256, 0, 0, originmins[0], originmins[1], originmins[2], originmaxs[0], originmaxs[1], originmaxs[2], 0, 0, 0, 0);
+ else if (effectnameindex == EFFECT_TE_TEI_SMOKE)
+ {
+ if (cl_particles_smoke.integer)
+ {
+ count *= 0.25f * cl_particles_quality.value;
+ while (count-- > 0)
+ particle(particletype + pt_smoke, 0x202020, 0x404040, tex_smoke[rand()&7], 5, 0, 255, 512, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 1.5f, 6.0f);
+ }
+ }
+ else if (effectnameindex == EFFECT_TE_TEI_BIGEXPLOSION)
+ {
+ CL_ParticleExplosion(center);
+ CL_AllocLightFlash(NULL, &tempmatrix, 500, 2.5f, 2.0f, 1.0f, 500, 9999, 0, -1, true, 1, 0.25, 0.5, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_TE_TEI_PLASMAHIT)
+ {
+ float f;
+ if (cl_stainmaps.integer)
+ R_Stain(center, 40, 96, 96, 96, 40, 128, 128, 128, 40);
+ CL_SpawnDecalParticleForPoint(center, 6, 8, 255, tex_bulletdecal[rand()&7], 0xFFFFFF, 0xFFFFFF);
+ if (cl_particles_smoke.integer)
+ for (f = 0;f < count;f += 4.0f / cl_particles_quality.value)
+ particle(particletype + pt_smoke, 0x202020, 0x404040, tex_smoke[rand()&7], 5, 0, 255, 512, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 20, 155);
+ if (cl_particles_sparks.integer)
+ for (f = 0;f < count;f += 1.0f / cl_particles_quality.value)
+ particle(particletype + pt_spark, 0x2030FF, 0x80C0FF, tex_particle, 2.0f, 0, lhrandom(64, 255), 512, 0, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0, 0, 0, 465);
+ CL_AllocLightFlash(NULL, &tempmatrix, 500, 0.6f, 1.2f, 2.0f, 2000, 9999, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_EF_FLAME)
+ {
+ count *= 300 * cl_particles_quality.value;
+ while (count-- > 0)
+ particle(particletype + pt_smoke, 0x6f0f00, 0xe3974f, tex_particle, 4, 0, lhrandom(64, 128), 384, -1, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 16, 128);
+ CL_AllocLightFlash(NULL, &tempmatrix, 200, 2.0f, 1.5f, 0.5f, 0, 0, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (effectnameindex == EFFECT_EF_STARDUST)
+ {
+ count *= 200 * cl_particles_quality.value;
+ while (count-- > 0)
+ particle(particletype + pt_static, 0x903010, 0xFFD030, tex_particle, 4, 0, lhrandom(64, 128), 128, 1, 0, lhrandom(originmins[0], originmaxs[0]), lhrandom(originmins[1], originmaxs[1]), lhrandom(originmins[2], originmaxs[2]), lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 0.2, 0.8, 16, 128);
+ CL_AllocLightFlash(NULL, &tempmatrix, 200, 1.0f, 0.7f, 0.3f, 0, 0, 0, -1, true, 1, 0.25, 0.25, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else if (!strncmp(particleeffectname[effectnameindex], "TR_", 3))
+ {
+ vec3_t dir, pos;
+ float len, dec, qd;
+ int smoke, blood, bubbles, r, color;
+
+ if (spawndlight && r_refdef.numlights < MAX_DLIGHTS)
+ {
+ vec4_t light;
+ Vector4Set(light, 0, 0, 0, 0);
+
+ if (effectnameindex == EFFECT_TR_ROCKET)
+ Vector4Set(light, 3.0f, 1.5f, 0.5f, 200);
+ else if (effectnameindex == EFFECT_TR_VORESPIKE)
+ {
+ if (gamemode == GAME_PRYDON && !cl_particles_quake.integer)
+ Vector4Set(light, 0.3f, 0.6f, 1.2f, 100);
+ else
+ Vector4Set(light, 1.2f, 0.5f, 1.0f, 200);
+ }
+ else if (effectnameindex == EFFECT_TR_NEXUIZPLASMA)
+ Vector4Set(light, 0.75f, 1.5f, 3.0f, 200);
+
+ if (light[3])
+ {
+ matrix4x4_t tempmatrix;
+ Matrix4x4_CreateFromQuakeEntity(&tempmatrix, originmaxs[0], originmaxs[1], originmaxs[2], 0, 0, 0, light[3]);
+ R_RTLight_Update(&r_refdef.lights[r_refdef.numlights++], false, &tempmatrix, light, -1, NULL, true, 1, 0.25, 0, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ }
+
+ if (!spawnparticles)
+ return;
+
+ if (originmaxs[0] == originmins[0] && originmaxs[1] == originmins[1] && originmaxs[2] == originmins[2])
+ return;
+
+ VectorSubtract(originmaxs, originmins, dir);
+ len = VectorNormalizeLength(dir);
+ if (ent)
+ {
+ dec = -ent->persistent.trail_time;
+ ent->persistent.trail_time += len;
+ if (ent->persistent.trail_time < 0.01f)
+ return;
+
+ // if we skip out, leave it reset
+ ent->persistent.trail_time = 0.0f;
+ }
+ else
+ dec = 0;
+
+ // advance into this frame to reach the first puff location
+ VectorMA(originmins, dec, dir, pos);
+ len -= dec;
+
+ smoke = cl_particles.integer && cl_particles_smoke.integer;
+ blood = cl_particles.integer && cl_particles_blood.integer;
+ bubbles = cl_particles.integer && cl_particles_bubbles.integer && !cl_particles_quake.integer && (CL_PointSuperContents(pos) & (SUPERCONTENTS_WATER | SUPERCONTENTS_SLIME));
+ qd = 1.0f / cl_particles_quality.value;
+
+ while (len >= 0)
+ {
+ dec = 3;
+ if (blood)
+ {
+ if (effectnameindex == EFFECT_TR_BLOOD)
+ {
+ if (cl_particles_quake.integer)
+ {
+ color = particlepalette[67 + (rand()&3)];
+ particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 128, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 3, 0);
+ }
+ else
+ {
+ dec = 16;
+ particle(particletype + pt_blood, 0xFFFFFF, 0xFFFFFF, tex_bloodparticle[rand()&7], 8, 0, qd * cl_particles_blood_alpha.value * 768.0f, qd * cl_particles_blood_alpha.value * 384.0f, 0, -1, pos[0], pos[1], pos[2], lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 0, 64);
+ }
+ }
+ else if (effectnameindex == EFFECT_TR_SLIGHTBLOOD)
+ {
+ if (cl_particles_quake.integer)
+ {
+ dec = 6;
+ color = particlepalette[67 + (rand()&3)];
+ particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 128, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 3, 0);
+ }
+ else
+ {
+ dec = 32;
+ particle(particletype + pt_blood, 0xFFFFFF, 0xFFFFFF, tex_bloodparticle[rand()&7], 8, 0, qd * cl_particles_blood_alpha.value * 768.0f, qd * cl_particles_blood_alpha.value * 384.0f, 0, -1, pos[0], pos[1], pos[2], lhrandom(velocitymins[0], velocitymaxs[0]), lhrandom(velocitymins[1], velocitymaxs[1]), lhrandom(velocitymins[2], velocitymaxs[2]), 1, 4, 0, 64);
+ }
+ }
+ }
+ if (smoke)
+ {
+ if (effectnameindex == EFFECT_TR_ROCKET)
+ {
+ if (cl_particles_quake.integer)
+ {
+ r = rand()&3;
+ color = particlepalette[ramp3[r]];
+ particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 42*(6-r), 306, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 3, 0);
+ }
+ else
+ {
+ particle(particletype + pt_smoke, 0x303030, 0x606060, tex_smoke[rand()&7], 3, 0, cl_particles_smoke_alpha.value*62, cl_particles_smoke_alphafade.value*62, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+ particle(particletype + pt_static, 0x801010, 0xFFA020, tex_smoke[rand()&7], 3, 0, cl_particles_smoke_alpha.value*288, cl_particles_smoke_alphafade.value*1400, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 20);
+ }
+ }
+ else if (effectnameindex == EFFECT_TR_GRENADE)
+ {
+ if (cl_particles_quake.integer)
+ {
+ r = 2 + (rand()%5);
+ color = particlepalette[ramp3[r]];
+ particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 42*(6-r), 306, 0, -0.05, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 3, 0);
+ }
+ else
+ {
+ particle(particletype + pt_smoke, 0x303030, 0x606060, tex_smoke[rand()&7], 3, 0, cl_particles_smoke_alpha.value*50, cl_particles_smoke_alphafade.value*75, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+ }
+ }
+ else if (effectnameindex == EFFECT_TR_WIZSPIKE)
+ {
+ if (cl_particles_quake.integer)
+ {
+ dec = 6;
+ color = particlepalette[52 + (rand()&7)];
+ particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*dir[1], 30*-dir[0], 0, 0, 0, 0, 0);
+ particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*-dir[1], 30*dir[0], 0, 0, 0, 0, 0);
+ }
+ else if (gamemode == GAME_GOODVSBAD2)
+ {
+ dec = 6;
+ particle(particletype + pt_static, 0x00002E, 0x000030, tex_particle, 6, 0, 128, 384, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+ }
+ else
+ {
+ color = particlepalette[20 + (rand()&7)];
+ particle(particletype + pt_static, color, color, tex_particle, 2, 0, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+ }
+ }
+ else if (effectnameindex == EFFECT_TR_KNIGHTSPIKE)
+ {
+ if (cl_particles_quake.integer)
+ {
+ dec = 6;
+ color = particlepalette[230 + (rand()&7)];
+ particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*dir[1], 30*-dir[0], 0, 0, 0, 0, 0);
+ particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 512, 0, 0, pos[0], pos[1], pos[2], 30*-dir[1], 30*dir[0], 0, 0, 0, 0, 0);
+ }
+ else
+ {
+ color = particlepalette[226 + (rand()&7)];
+ particle(particletype + pt_static, color, color, tex_particle, 2, 0, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+ }
+ }
+ else if (effectnameindex == EFFECT_TR_VORESPIKE)
+ {
+ if (cl_particles_quake.integer)
+ {
+ color = particlepalette[152 + (rand()&3)];
+ particle(particletype + pt_alphastatic, color, color, tex_particle, 1.5f, 0, 255, 850, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 8, 0);
+ }
+ else if (gamemode == GAME_GOODVSBAD2)
+ {
+ dec = 6;
+ particle(particletype + pt_alphastatic, particlepalette[0 + (rand()&255)], particlepalette[0 + (rand()&255)], tex_particle, 6, 0, 255, 384, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+ }
+ else if (gamemode == GAME_PRYDON)
+ {
+ dec = 6;
+ particle(particletype + pt_static, 0x103040, 0x204050, tex_particle, 6, 0, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+ }
+ else
+ particle(particletype + pt_static, 0x502030, 0x502030, tex_particle, 3, 0, 64, 192, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+ }
+ else if (effectnameindex == EFFECT_TR_NEHAHRASMOKE)
+ {
+ dec = 7;
+ particle(particletype + pt_alphastatic, 0x303030, 0x606060, tex_smoke[rand()&7], 7, 0, 64, 320, 0, 0, pos[0], pos[1], pos[2], 0, 0, lhrandom(4, 12), 0, 0, 0, 4);
+ }
+ else if (effectnameindex == EFFECT_TR_NEXUIZPLASMA)
+ {
+ dec = 4;
+ particle(particletype + pt_static, 0x283880, 0x283880, tex_particle, 4, 0, 255, 1024, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 16);
+ }
+ else if (effectnameindex == EFFECT_TR_GLOWTRAIL)
+ particle(particletype + pt_alphastatic, particlepalette[palettecolor], particlepalette[palettecolor], tex_particle, 5, 0, 128, 320, 0, 0, pos[0], pos[1], pos[2], 0, 0, 0, 0, 0, 0, 0);
+ }
+ if (bubbles)
+ {
+ if (effectnameindex == EFFECT_TR_ROCKET)
+ particle(particletype + pt_bubble, 0x404040, 0x808080, tex_bubble, 2, 0, lhrandom(64, 255), 256, -0.25, 1.5, pos[0], pos[1], pos[2], 0, 0, 0, 0.0625, 0.25, 0, 16);
+ else if (effectnameindex == EFFECT_TR_GRENADE)
+ particle(particletype + pt_bubble, 0x404040, 0x808080, tex_bubble, 2, 0, lhrandom(64, 255), 256, -0.25, 1.5, pos[0], pos[1], pos[2], 0, 0, 0, 0.0625, 0.25, 0, 16);
+ }
+ // advance to next time and position
+ dec *= qd;
+ len -= dec;
+ VectorMA (pos, dec, dir, pos);
+ }
+ if (ent)
+ ent->persistent.trail_time = len;
+ }
+ else if (developer.integer >= 1)
+ Con_Printf("CL_ParticleEffect_Fallback: no fallback found for effect %s\n", particleeffectname[effectnameindex]);
+}
+
+// this is also called on point effects with spawndlight = true and
+// spawnparticles = true
+// it is called CL_ParticleTrail because most code does not want to supply
+// these parameters, only trail handling does
+void CL_ParticleTrail(int effectnameindex, float pcount, const vec3_t originmins, const vec3_t originmaxs, const vec3_t velocitymins, const vec3_t velocitymaxs, entity_t *ent, int palettecolor, qboolean spawndlight, qboolean spawnparticles)
+{
+ vec3_t center;
+ qboolean found = false;
+ if (effectnameindex < 1 || effectnameindex >= MAX_PARTICLEEFFECTNAME)
+ return; // invalid effect index
+ if (!particleeffectname[effectnameindex][0])
+ return; // no such effect
+ VectorLerp(originmins, 0.5, originmaxs, center);
+ if (!cl_particles_quake.integer && particleeffectinfo[0].effectnameindex)
+ {
+ int effectinfoindex;
+ int supercontents;
+ int tex;
+ particleeffectinfo_t *info;
+ vec3_t center;
+ vec3_t centervelocity;
+ vec3_t traildir;
+ vec3_t trailpos;
+ vec3_t rvec;
+ vec_t traillen;
+ vec_t trailstep;
+ qboolean underwater;
+ // note this runs multiple effects with the same name, each one spawns only one kind of particle, so some effects need more than one
+ VectorLerp(originmins, 0.5, originmaxs, center);
+ VectorLerp(velocitymins, 0.5, velocitymaxs, centervelocity);
+ supercontents = CL_PointSuperContents(center);
+ underwater = (supercontents & (SUPERCONTENTS_WATER | SUPERCONTENTS_SLIME)) != 0;
+ VectorSubtract(originmaxs, originmins, traildir);
+ traillen = VectorLength(traildir);
+ VectorNormalize(traildir);
+ for (effectinfoindex = 0, info = particleeffectinfo;effectinfoindex < MAX_PARTICLEEFFECTINFO && info->effectnameindex;effectinfoindex++, info++)
+ {
+ if (info->effectnameindex == effectnameindex)
+ {
+ found = true;
+ if ((info->flags & PARTICLEEFFECT_UNDERWATER) && !underwater)
+ continue;
+ if ((info->flags & PARTICLEEFFECT_NOTUNDERWATER) && underwater)
+ continue;
+
+ // spawn a dlight if requested
+ if (info->lightradiusstart > 0 && spawndlight)
+ {
+ matrix4x4_t tempmatrix;
+ if (info->trailspacing > 0)
+ Matrix4x4_CreateTranslate(&tempmatrix, originmaxs[0], originmaxs[1], originmaxs[2]);
+ else
+ Matrix4x4_CreateTranslate(&tempmatrix, center[0], center[1], center[2]);
+ if (info->lighttime > 0 && info->lightradiusfade > 0)
+ {
+ // light flash (explosion, etc)
+ // called when effect starts
+ CL_AllocLightFlash(NULL, &tempmatrix, info->lightradiusstart, info->lightcolor[0], info->lightcolor[1], info->lightcolor[2], info->lightradiusfade, info->lighttime, info->lightcubemapnum, -1, info->lightshadow, 1, 0.25, 0, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ else
+ {
+ // glowing entity
+ // called by CL_LinkNetworkEntity
+ Matrix4x4_Scale(&tempmatrix, info->lightradiusstart, 1);
+ R_RTLight_Update(&r_refdef.lights[r_refdef.numlights++], false, &tempmatrix, info->lightcolor, -1, info->lightcubemapnum > 0 ? va("cubemaps/%i", info->lightcubemapnum) : NULL, info->lightshadow, 1, 0.25, 0, 1, 1, LIGHTFLAG_NORMALMODE | LIGHTFLAG_REALTIMEMODE);
+ }
+ }