Skip to content

Commit eece86f

Browse files
ernieRippeR37
authored andcommitted
Add cg_bloodParticles
Replaces 5-frame cartoonish blood sprite with particles. They leave blood decals if they hit a surface.
1 parent 8cea39d commit eece86f

File tree

5 files changed

+276
-23
lines changed

5 files changed

+276
-23
lines changed

code/cgame/cg_effects.c

Lines changed: 183 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -538,32 +538,199 @@ localEntity_t *CG_MakeExplosion( vec3_t origin, vec3_t dir,
538538
=================
539539
CG_Bleed
540540
541-
This is the spurt of blood when a character gets hit
541+
This is the spurt of blood when a character gets hit.
542+
Spawns multiple blood droplet particles that spray along the impact direction.
543+
dir = direction the projectile was traveling (NULL for omnidirectional spray)
544+
weapon = weapon type for determining particle count
542545
=================
543546
*/
544-
void CG_Bleed( vec3_t origin, int entityNum ) {
545-
localEntity_t *ex;
547+
#define BLOOD_PARTICLE_SPEED_EXIT 600
548+
#define BLOOD_PARTICLE_SPEED_ENTRY 50
549+
550+
static int CG_BloodParticleCount( int weapon ) {
551+
// Returns droplet count (reduced by 1 since we also spawn a puff)
552+
switch ( weapon ) {
553+
case WP_PLASMAGUN:
554+
return 4;
555+
case WP_BFG:
556+
return 7;
557+
case WP_ROCKET_LAUNCHER:
558+
case WP_GRENADE_LAUNCHER:
559+
return 9;
560+
// Weapons that trigger CG_Bleed via EV_MISSILE_HIT or EV_BULLET_HIT_FLESH:
561+
// Gauntlet, machine gun, shotgun, lightning gun, or even grappling hook
562+
// Railgun doesn't trigger a blood event.
563+
default:
564+
return 2;
565+
}
566+
}
567+
568+
void CG_Bleed( vec3_t origin, vec3_t dir, int entityNum, int weapon ) {
569+
localEntity_t *le;
570+
refEntity_t *re;
571+
int i;
572+
int particleCount;
573+
vec3_t velocity;
574+
vec3_t baseDir;
575+
vec3_t perpA, perpB;
576+
float spread, forwardBias;
577+
float speed;
578+
qboolean isPlayer;
546579

547580
if ( !cg_blood.integer ) {
548581
return;
549582
}
550583

551-
ex = CG_AllocLocalEntity();
552-
ex->leType = LE_EXPLOSION;
584+
isPlayer = ( entityNum == cg.snap->ps.clientNum );
553585

554-
ex->startTime = cg.time;
555-
ex->endTime = ex->startTime + 500;
556-
557-
VectorCopy ( origin, ex->refEntity.origin);
558-
ex->refEntity.reType = RT_SPRITE;
559-
ex->refEntity.rotation = rand() % 360;
560-
ex->refEntity.radius = 24;
586+
// If particles disabled, use original sprite-based blood effect
587+
if ( !cg_bloodParticles.integer ) {
588+
le = CG_AllocLocalEntity();
589+
le->leType = LE_EXPLOSION;
590+
le->startTime = cg.time;
591+
le->endTime = le->startTime + 500;
592+
593+
VectorCopy( origin, le->refEntity.origin );
594+
le->refEntity.reType = RT_SPRITE;
595+
le->refEntity.rotation = rand() % 360;
596+
le->refEntity.radius = 24;
597+
le->refEntity.customShader = cgs.media.bloodExplosionShader;
561598

562-
ex->refEntity.customShader = cgs.media.bloodExplosionShader;
599+
// don't show player's own blood in view
600+
if ( isPlayer ) {
601+
le->refEntity.renderfx |= RF_THIRD_PERSON;
602+
}
603+
return;
604+
}
563605

564-
// don't show player's own blood in view
565-
if ( entityNum == cg.snap->ps.clientNum ) {
566-
ex->refEntity.renderfx |= RF_THIRD_PERSON;
606+
qboolean inLiquid = ( CG_PointContents( origin, -1 ) & MASK_WATER ) != 0;
607+
608+
if ( inLiquid ) {
609+
// Single puff underwater
610+
float puffRadius = 2 + random() * 3; // 2-5 units
611+
int puffDuration = 300 + random() * 200; // 300-500ms
612+
613+
le = CG_SmokePuff( origin, vec3_origin,
614+
puffRadius,
615+
1, 1, 1, 1,
616+
puffDuration,
617+
cg.time, 0, 0,
618+
cgs.media.bloodTrailShader );
619+
le->leType = LE_FALL_SCALE_FADE;
620+
le->pos.trDelta[2] = -2; // Slow rise
621+
622+
if ( isPlayer ) {
623+
le->refEntity.renderfx |= RF_THIRD_PERSON;
624+
}
625+
return;
626+
}
627+
628+
particleCount = CG_BloodParticleCount( weapon );
629+
630+
// Set up directional basis if we have a direction
631+
if ( dir && ( dir[0] != 0 || dir[1] != 0 || dir[2] != 0 ) ) {
632+
VectorNormalize2( dir, baseDir );
633+
// Create perpendicular vectors for spray spread
634+
PerpendicularVector( perpA, baseDir );
635+
CrossProduct( baseDir, perpA, perpB );
636+
} else {
637+
// No direction - use upward as default
638+
VectorSet( baseDir, 0, 0, 1 );
639+
VectorSet( perpA, 1, 0, 0 );
640+
VectorSet( perpB, 0, 1, 0 );
641+
}
642+
643+
// Spawn the blood droplet particles
644+
for ( i = 0; i < particleCount; i++ ) {
645+
le = CG_AllocLocalEntity();
646+
re = &le->refEntity;
647+
648+
le->leFlags = LEF_PUFF_DONT_SCALE;
649+
le->leType = LE_BLOOD_PARTICLE;
650+
le->startTime = cg.time;
651+
le->endTime = cg.time + 800 + random() * 400;
652+
le->lifeRate = 1.0f / ( le->endTime - le->startTime );
653+
654+
// Directional spray along bullet path
655+
// 80% exit wound (away from shooter), 20% entry (random horizontal splash)
656+
qboolean isExitWound = ( random() < 0.8f );
657+
if ( isExitWound ) {
658+
speed = BLOOD_PARTICLE_SPEED_EXIT * (0.4f + random() * 0.6f);
659+
forwardBias = 0.8f + random() * 0.4f;
660+
spread = (random() - 0.5f) * 0.4f; // Tight perpendicular spread
661+
662+
velocity[0] = baseDir[0] * speed * forwardBias
663+
+ perpA[0] * speed * spread
664+
+ perpB[0] * speed * (random() - 0.5f) * 0.6f;
665+
velocity[1] = baseDir[1] * speed * forwardBias
666+
+ perpA[1] * speed * spread
667+
+ perpB[1] * speed * (random() - 0.5f) * 0.6f;
668+
velocity[2] = baseDir[2] * speed * forwardBias
669+
+ perpA[2] * speed * spread
670+
+ perpB[2] * speed * (random() - 0.5f) * 0.6f
671+
+ speed * 0.2f; // Slight upward bias
672+
} else {
673+
// Entry wound - spray in the plane perpendicular to projectile direction
674+
float angle = random() * M_PI * 2;
675+
float perpSpeed;
676+
speed = BLOOD_PARTICLE_SPEED_ENTRY * (0.4f + random() * 0.6f);
677+
perpSpeed = speed * (0.8f + random() * 0.4f);
678+
679+
velocity[0] = perpA[0] * cos( angle ) * perpSpeed + perpB[0] * sin( angle ) * perpSpeed;
680+
velocity[1] = perpA[1] * cos( angle ) * perpSpeed + perpB[1] * sin( angle ) * perpSpeed;
681+
velocity[2] = perpA[2] * cos( angle ) * perpSpeed + perpB[2] * sin( angle ) * perpSpeed;
682+
}
683+
684+
le->pos.trType = TR_GRAVITY;
685+
le->pos.trTime = cg.time;
686+
VectorCopy( origin, le->pos.trBase );
687+
VectorCopy( velocity, le->pos.trDelta );
688+
689+
// Use bloodTrail shader for particles
690+
re->reType = RT_SPRITE;
691+
re->rotation = rand() % 360;
692+
re->radius = 3 + random() * 5; // Varied sizes 3-8
693+
re->customShader = cgs.media.bloodTrailShader;
694+
re->shaderTime = cg.time / 1000.0f;
695+
696+
VectorCopy( le->pos.trBase, re->origin );
697+
698+
// Set color (shader handles actual blood color)
699+
le->color[0] = 1.0f;
700+
le->color[1] = 1.0f;
701+
le->color[2] = 1.0f;
702+
le->color[3] = 1.0f;
703+
704+
re->shaderRGBA[0] = 0xff;
705+
re->shaderRGBA[1] = 0xff;
706+
re->shaderRGBA[2] = 0xff;
707+
re->shaderRGBA[3] = 0xff;
708+
709+
le->radius = re->radius;
710+
711+
// don't show player's own blood in view
712+
if ( isPlayer ) {
713+
re->renderfx |= RF_THIRD_PERSON;
714+
}
715+
}
716+
717+
// Add a blood mist at entry or exit wound
718+
{
719+
float puffRadius = 2 + random() * 3; // 2-5 units
720+
int puffDuration = 300 + random() * 200; // 300-500ms
721+
722+
le = CG_SmokePuff( origin, vec3_origin,
723+
puffRadius,
724+
1, 1, 1, 1,
725+
puffDuration,
726+
cg.time, 0, 0,
727+
cgs.media.bloodTrailShader );
728+
le->leType = LE_FALL_SCALE_FADE;
729+
le->pos.trDelta[2] = 4; // Slow fall
730+
731+
if ( isPlayer ) {
732+
le->refEntity.renderfx |= RF_THIRD_PERSON;
733+
}
567734
}
568735
}
569736

code/cgame/cg_local.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
5959
#define MAX_STEP_CHANGE 32
6060

6161
#define MAX_VERTS_ON_POLY 10
62-
#define MAX_MARK_POLYS 256
62+
#define MAX_MARK_POLYS 512
6363

6464
#define STAT_MINUS 10 // num frame for '-' stats digit
6565

@@ -230,6 +230,7 @@ typedef enum {
230230
LE_SCALE_FADE,
231231
LE_SCOREPLUM,
232232
LE_DAMAGEPLUM,
233+
LE_BLOOD_PARTICLE,
233234
#ifdef MISSIONPACK
234235
LE_KAMIKAZE,
235236
LE_INVULIMPACT,
@@ -1237,6 +1238,7 @@ extern vmCvar_t cg_forceModel;
12371238
extern vmCvar_t cg_buildScript;
12381239
extern vmCvar_t cg_paused;
12391240
extern vmCvar_t cg_blood;
1241+
extern vmCvar_t cg_bloodParticles;
12401242
extern vmCvar_t cg_damageEffect;
12411243
extern vmCvar_t cg_predictItems;
12421244
extern vmCvar_t cg_deferPlayers;
@@ -1550,7 +1552,7 @@ void CG_DamagePlum( vec3_t org, int damage );
15501552
void CG_GibPlayer( vec3_t playerOrigin );
15511553
void CG_BigExplode( vec3_t playerOrigin );
15521554

1553-
void CG_Bleed( vec3_t origin, int entityNum );
1555+
void CG_Bleed( vec3_t origin, vec3_t dir, int entityNum, int weapon );
15541556

15551557
localEntity_t *CG_MakeExplosion( vec3_t origin, vec3_t dir,
15561558
qhandle_t hModel, qhandle_t shader, int msec,

code/cgame/cg_localents.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,62 @@ static void CG_AddMoveScaleFade( localEntity_t *le ) {
389389
}
390390

391391

392+
/*
393+
===================
394+
CG_AddBloodParticle
395+
396+
Blood droplet that moves with gravity, traces for collision,
397+
leaves a blood mark on impact, then fades out on the surface.
398+
===================
399+
*/
400+
static void CG_AddBloodParticle( localEntity_t *le ) {
401+
refEntity_t *re;
402+
vec3_t newOrigin;
403+
trace_t trace;
404+
float c;
405+
406+
re = &le->refEntity;
407+
408+
// Calculate fade
409+
c = ( le->endTime - cg.time ) * le->lifeRate;
410+
if ( c < 0 ) c = 0;
411+
re->shaderRGBA[3] = 0xff * c * le->color[3];
412+
413+
// Calculate new position
414+
BG_EvaluateTrajectory( &le->pos, cg.time, newOrigin );
415+
416+
// Particle entered water - spawn sinking blood cloud
417+
if ( CG_PointContents( newOrigin, -1 ) & MASK_WATER ) {
418+
localEntity_t *cloud;
419+
420+
cloud = CG_SmokePuff( newOrigin, vec3_origin,
421+
1 + random() * 2, 1, 1, 1, 0.4f,
422+
300 + random() * 200, cg.time, 0, 0,
423+
cgs.media.bloodTrailShader );
424+
cloud->leType = LE_FALL_SCALE_FADE;
425+
cloud->pos.trDelta[2] = 5 + random() * 10;
426+
CG_FreeLocalEntity( le );
427+
return;
428+
}
429+
430+
// Trace for collision
431+
CG_Trace( &trace, re->origin, NULL, NULL, newOrigin, -1, CONTENTS_SOLID );
432+
433+
if ( trace.fraction < 1.0f ) {
434+
// Hit a surface - leave a matching mark
435+
CG_ImpactMark( cgs.media.bloodMarkShader, trace.endpos, trace.plane.normal,
436+
random() * 360, 1, 1, 1, 1, qtrue, le->radius, qfalse );
437+
CG_FreeLocalEntity( le );
438+
return;
439+
} else {
440+
// Still in flight
441+
VectorCopy( newOrigin, re->origin );
442+
}
443+
444+
trap_R_AddRefEntityToScene( re );
445+
}
446+
447+
392448
/*
393449
===================
394450
CG_AddScaleFade
@@ -963,6 +1019,10 @@ void CG_AddLocalEntities( void ) {
9631019
CG_AddDamagePlum( le );
9641020
break;
9651021

1022+
case LE_BLOOD_PARTICLE:
1023+
CG_AddBloodParticle( le );
1024+
break;
1025+
9661026
#ifdef MISSIONPACK
9671027
case LE_KAMIKAZE:
9681028
CG_AddKamikaze( le );

code/cgame/cg_main.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ vmCvar_t cg_buildScript;
181181
vmCvar_t cg_forceModel;
182182
vmCvar_t cg_paused;
183183
vmCvar_t cg_blood;
184+
vmCvar_t cg_bloodParticles;
184185
vmCvar_t cg_damageEffect;
185186
vmCvar_t cg_predictItems;
186187
vmCvar_t cg_deferPlayers;
@@ -341,6 +342,7 @@ static cvarTable_t cvarTable[] = {
341342
{ &cg_buildScript, "com_buildScript", "0", 0 }, // force loading of all possible data amd error on failures
342343
{ &cg_paused, "cl_paused", "0", CVAR_ROM },
343344
{ &cg_blood, "com_blood", "1", CVAR_ARCHIVE },
345+
{ &cg_bloodParticles, "cg_bloodParticles", "0", CVAR_ARCHIVE },
344346
{ &cg_damageEffect, "cg_damageEffect", "0", CVAR_ARCHIVE },
345347
{ &cg_synchronousClients, "g_synchronousClients", "0", CVAR_SYSTEMINFO },
346348
#ifdef MISSIONPACK

0 commit comments

Comments
 (0)