Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/cgame/cg_cvar.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ CG_CVAR( cg_timescaleFadeEnd, "cg_timescaleFadeEnd", "1", 0 )
CG_CVAR( cg_timescaleFadeSpeed, "cg_timescaleFadeSpeed", "0", 0 )
CG_CVAR( cg_timescale, "timescale", "1", 0 )
CG_CVAR( cg_scorePlum, "cg_scorePlums", "1", CVAR_USERINFO | CVAR_ARCHIVE )
CG_CVAR( cg_damagePlums, "cg_damagePlums", "0", CVAR_USERINFO | CVAR_ARCHIVE )
CG_CVAR( cg_smoothClients, "cg_smoothClients", "0", CVAR_USERINFO | CVAR_ARCHIVE )
CG_CVAR( cg_cameraMode, "com_cameraMode", "0", CVAR_CHEAT )
CG_CVAR( cg_noTaunt, "cg_noTaunt", "0", CVAR_ARCHIVE )
Expand Down
46 changes: 46 additions & 0 deletions code/cgame/cg_effects.c
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,52 @@ void CG_ScorePlum( int client, const vec3_t origin, int score ) {
AnglesToAxis( angles, re->axis );
}

/*
==================
CG_DamagePlum
==================
*/
void CG_DamagePlum( vec3_t org, int damage ) {
localEntity_t *le;
refEntity_t *re;
vec3_t angles;
float random_x, random_y;

if ( cg_damagePlums.integer == 0 ) {
return;
}

le = CG_AllocLocalEntity();
le->leFlags = 0;
le->leType = LE_DAMAGEPLUM;
le->startTime = cg.time;
le->endTime = cg.time + 1000;
le->lifeRate = 1.0 / ( le->endTime - le->startTime );

le->color[0] = 1.0;
le->color[1] = 0.5;
le->color[2] = 0.0;
le->color[3] = 1.0;
le->radius = damage;

VectorCopy( org, le->pos.trBase );

random_x = (random() * 2.0 - 1.0);
random_y = (random() * 2.0 - 1.0);

le->pos.trDelta[0] = random_x;
le->pos.trDelta[1] = random_y;
le->pos.trDelta[2] = 0.5 + random() * 0.5;

re = &le->refEntity;

re->reType = RT_SPRITE;
re->radius = 16;

VectorClear(angles);
AnglesToAxis( angles, re->axis );
}


/*
====================
Expand Down
4 changes: 4 additions & 0 deletions code/cgame/cg_event.c
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,10 @@ void CG_EntityEvent( centity_t *cent, vec3_t position, int entityNum ) {
CG_ScorePlum( cent->currentState.otherEntityNum, cent->lerpOrigin, cent->currentState.time );
break;

case EV_DAMAGEPLUM:
CG_DamagePlum( cent->lerpOrigin, cent->currentState.time );
break;

//
// missile impacts
//
Expand Down
2 changes: 2 additions & 0 deletions code/cgame/cg_local.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ typedef enum {
LE_FADE_RGB,
LE_SCALE_FADE,
LE_SCOREPLUM,
LE_DAMAGEPLUM,
#ifdef MISSIONPACK
LE_KAMIKAZE,
LE_INVULIMPACT,
Expand Down Expand Up @@ -1405,6 +1406,7 @@ void CG_InvulnerabilityJuiced( vec3_t org );
void CG_LightningBoltBeam( vec3_t start, vec3_t end );
#endif
void CG_ScorePlum( int client, const vec3_t origin, int score );
void CG_DamagePlum( vec3_t org, int damage );

void CG_GibPlayer( const vec3_t playerOrigin );
void CG_BigExplode( vec3_t playerOrigin );
Expand Down
107 changes: 107 additions & 0 deletions code/cgame/cg_localents.c
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,109 @@ void CG_AddScorePlum( localEntity_t *le ) {
}
}

/*
===================
CG_AddDamagePlum
===================
*/
void CG_AddDamagePlum( localEntity_t *le ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There really is a lot of common code between CG_AddScorePlum and this. Maybe some could be factored out?

Although maybe not, the original code is also full of copy-paste.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my thinking. It’s weird but also follows the style of the original

refEntity_t *re;
vec3_t origin, delta, dir, vec, up = {0, 0, 1};
float c, len, distance;
int i, damage, digits[10], numdigits, negative;
float progress, fade, spread_x, spread_y, vertical_offset, peak_height;

re = &le->refEntity;

c = ( le->endTime - cg.time ) * le->lifeRate;

damage = le->radius;

// Color based on damage amount - gradient from blue to red
if (damage > 75) {
// Red
re->shaderRGBA[0] = 0xff;
re->shaderRGBA[1] = 0x00;
re->shaderRGBA[2] = 0x00;
} else if (damage > 50) {
// Orange
re->shaderRGBA[0] = 0xff;
re->shaderRGBA[1] = 0x80;
re->shaderRGBA[2] = 0x00;
} else if (damage > 25) {
// Yellow
re->shaderRGBA[0] = 0xff;
re->shaderRGBA[1] = 0xff;
re->shaderRGBA[2] = 0x00;
} else {
// Blue
re->shaderRGBA[0] = 0x00;
re->shaderRGBA[1] = 0x80;
re->shaderRGBA[2] = 0xff;
}

// Fade out after 75% of arc (750ms)
progress = 1.0 - c; // 0.0 at start, 1.0 at end
if (progress < 0.75f) {
fade = 1.0f; // Full opacity for first 750ms
} else {
fade = 1.0f - ((progress - 0.75f) / 0.25f); // Fade out over remaining 250ms
}
re->shaderRGBA[3] = 0xff * fade;

VectorCopy(le->pos.trBase, origin);

// Calculate distance to base origin for scaling sprite and arc
VectorSubtract( origin, cg.refdef.vieworg, delta );
len = VectorLengthSquared( delta );
if ( len < 20*20 ) {
// if the view would be "inside" the sprite, kill the sprite
CG_FreeLocalEntity( le );
return;
}

distance = sqrt(len);
re->radius = (NUMBER_SIZE / 1280.0f) * distance * tan(cg.refdef.fov_x * M_PI / 360.0f);

// Horizontal spread
spread_x = le->pos.trDelta[0] * 20.0 * re->radius * progress;
spread_y = le->pos.trDelta[1] * 20.0 * re->radius * progress;
origin[0] += spread_x;
origin[1] += spread_y;

// Vertical arc - symmetric rise and fall over the full duration
// Uses sine wave for smooth, even arc that peaks at 50% progress
peak_height = 15.0 * le->pos.trDelta[2] * re->radius;
vertical_offset = peak_height * sin(progress * M_PI);
origin[2] += vertical_offset;

VectorSubtract(cg.refdef.vieworg, origin, dir);
CrossProduct(dir, up, vec);
VectorNormalize(vec);

negative = qfalse;
if (damage < 0) {
negative = qtrue;
damage = -damage;
}

for (numdigits = 0; !(numdigits && !damage); numdigits++) {
digits[numdigits] = damage % 10;
damage = damage / 10;
}

if (negative) {
digits[numdigits] = 10;
numdigits++;
}

for (i = 0; i < numdigits; i++) {
VectorMA(origin, (float) (((float) numdigits / 2) - i) * (re->radius * 2), vec, re->origin);
re->customShader = cgs.media.numberShaders[digits[numdigits-1-i]];
trap_R_AddRefEntityToScene( re );
}
}




Expand Down Expand Up @@ -936,6 +1039,10 @@ void CG_AddLocalEntities( void ) {
CG_AddScorePlum( le );
break;

case LE_DAMAGEPLUM:
CG_AddDamagePlum( le );
break;

#ifdef MISSIONPACK
case LE_KAMIKAZE:
CG_AddKamikaze( le );
Expand Down
4 changes: 3 additions & 1 deletion code/game/bg_events.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ DECLARE_EVENT( EV_TAUNT_NO ),
DECLARE_EVENT( EV_TAUNT_FOLLOWME ),
DECLARE_EVENT( EV_TAUNT_GETFLAG ),
DECLARE_EVENT( EV_TAUNT_GUARDBASE ),
DECLARE_EVENT( EV_TAUNT_PATROL )
DECLARE_EVENT( EV_TAUNT_PATROL ),

DECLARE_EVENT( EV_DAMAGEPLUM ) // damage plum
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I've come up with a way to keep network compatibility!
This will display damage plums as score plums in older clients.

Image

0001-fix-don-t-introduce-new-event-reuse-EV_SCOREPLUM.patch

patch text
From 0cefcfcdc83db1ad2f9c263362ca704dca69cd40 Mon Sep 17 00:00:00 2001
From: WofWca <wofwca@protonmail.com>
Date: Sun, 1 Feb 2026 18:52:10 +0400
Subject: [PATCH] fix: don't introduce new event, reuse EV_SCOREPLUM

This fixes inability to play back demos that were recorded
in the new version on a client that doesn't know
about the `EV_DAMAGEPLUM` event. See
https://github.com/ec-/baseq3a/pull/56#issuecomment-3772877175.

This will display damage plums as score plums in older clients.
---
 code/cgame/cg_event.c | 10 +++++-----
 code/game/bg_events.h |  4 +---
 code/game/bg_public.h |  1 +
 code/game/g_combat.c  |  3 ++-
 4 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/code/cgame/cg_event.c b/code/cgame/cg_event.c
index 15390dcb..fe33a047 100644
--- a/code/cgame/cg_event.c
+++ b/code/cgame/cg_event.c
@@ -960,11 +960,11 @@ void CG_EntityEvent( centity_t *cent, vec3_t position, int entityNum ) {
 #endif
 
 	case EV_SCOREPLUM:
-		CG_ScorePlum( cent->currentState.otherEntityNum, cent->lerpOrigin, cent->currentState.time );
-		break;
-
-	case EV_DAMAGEPLUM:
-		CG_DamagePlum( cent->lerpOrigin, cent->currentState.time );
+		if (cent->currentState.eventParm & SCOREPLUM_IS_DAMAGEPLUM) {
+			CG_DamagePlum( cent->lerpOrigin, cent->currentState.time );
+		} else {
+			CG_ScorePlum( cent->currentState.otherEntityNum, cent->lerpOrigin, cent->currentState.time );
+		}
 		break;
 
 	//
diff --git a/code/game/bg_events.h b/code/game/bg_events.h
index 27d74e09..b9e0b2ce 100644
--- a/code/game/bg_events.h
+++ b/code/game/bg_events.h
@@ -108,9 +108,7 @@ DECLARE_EVENT( EV_TAUNT_NO ),
 DECLARE_EVENT( EV_TAUNT_FOLLOWME ),
 DECLARE_EVENT( EV_TAUNT_GETFLAG ),
 DECLARE_EVENT( EV_TAUNT_GUARDBASE ),
-DECLARE_EVENT( EV_TAUNT_PATROL ),
-
-DECLARE_EVENT( EV_DAMAGEPLUM )				// damage plum
+DECLARE_EVENT( EV_TAUNT_PATROL )
 
 #ifdef EVENT_ENUMS
 	, DECLARE_EVENT( EV_MAX )
diff --git a/code/game/bg_public.h b/code/game/bg_public.h
index c5374159..af02efe6 100644
--- a/code/game/bg_public.h
+++ b/code/game/bg_public.h
@@ -355,6 +355,7 @@ typedef enum {
 #undef EVENT_ENUMS
 } entity_event_t;
 
+#define SCOREPLUM_IS_DAMAGEPLUM 0x01
 
 typedef enum {
 	GTS_RED_CAPTURE,
diff --git a/code/game/g_combat.c b/code/game/g_combat.c
index 4639789a..9156bc84 100644
--- a/code/game/g_combat.c
+++ b/code/game/g_combat.c
@@ -36,13 +36,14 @@ void DamagePlum( gentity_t *attacker, vec3_t origin, int damage ) {
 		return;
 	}
 
-	plum = G_TempEntity( origin, EV_DAMAGEPLUM );
+	plum = G_TempEntity( origin, EV_SCOREPLUM );
 	// only send this temp entity to the attacker
 	plum->r.svFlags |= SVF_SINGLECLIENT;
 	plum->r.singleClient = attacker->s.number;
 	//
 	plum->s.otherEntityNum = attacker->s.number;
 	plum->s.time = damage;
+	plum->s.eventParm = SCOREPLUM_IS_DAMAGEPLUM;
 }
 
 /*
-- 
2.41.0

Maybe one should consider hiding them completely, e.g. by providing an out-of-range client so that the if (client != cg.predictedPlayerState.clientNum) return check is triggered, but I think it's OK to show them together with regular score plums.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid idea. I've stopped pushing over here but implemented in https://github.com/ernie/trinity (which has adopted the quake live style font, as well)


#ifdef EVENT_ENUMS
, DECLARE_EVENT( EV_MAX )
Expand Down
6 changes: 6 additions & 0 deletions code/game/g_active.c
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,12 @@ void ClientEndFrame( gentity_t *ent ) {
client->damage.team = 0;
}

// send accumulated damage plums
for ( i = 0; i < client->damagePlumCount; i++ ) {
DamagePlum( ent, client->damagePlums[i].origin, client->damagePlums[i].damage );
}
client->damagePlumCount = 0;

// set the bit for the reachability area the client is currently in
// i = trap_AAS_PointReachabilityAreaIndex( ent->client->ps.origin );
// ent->client->areabits[i >> 3] |= 1 << (i & 7);
Expand Down
7 changes: 7 additions & 0 deletions code/game/g_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,13 @@ qboolean ClientUserinfoChanged( int clientNum ) {
client->pers.predictItemPickup = qtrue;
}

// client wants damage plum data?
if ( atoi( Info_ValueForKey( userinfo, "cg_damagePlums" ) ) ) {
client->pers.damagePlums = qtrue;
} else {
client->pers.damagePlums = qfalse;
}

// set name
Q_strncpyz( oldname, client->pers.netname, sizeof( oldname ) );
s = Info_ValueForKey( userinfo, "name" );
Expand Down
41 changes: 41 additions & 0 deletions code/game/g_combat.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,27 @@ void ScorePlum( gentity_t *ent, vec3_t origin, int score ) {
plum->s.time = score;
}

/*
============
DamagePlum
============
*/
void DamagePlum( gentity_t *attacker, vec3_t origin, int damage ) {
gentity_t *plum;

if ( !attacker || !attacker->client || !attacker->client->pers.damagePlums) {
return;
}

plum = G_TempEntity( origin, EV_DAMAGEPLUM );
// only send this temp entity to the attacker
plum->r.svFlags |= SVF_SINGLECLIENT;
plum->r.singleClient = attacker->s.number;
//
plum->s.otherEntityNum = attacker->s.number;
plum->s.time = damage;
Comment on lines +37 to +43
Copy link
Contributor

@WofWca WofWca Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you followed the example of cg_scorePlum, but I wonder if we really need a server event for this? Can't the client do this not far from where the "hit" sound is played, i.e. CG_TransitionPlayerState?

void CG_TransitionPlayerState( playerState_t *ps, playerState_t *ops ) {
qboolean respawn;
// check for changing follow mode
if ( ps->clientNum != ops->clientNum ) {
cg.thisFrameTeleport = qtrue;
// make sure we don't get any unwanted transition effects
*ops = *ps;
}
// damage events (player is getting wounded)
if ( ps->damageEvent != ops->damageEvent && ps->damageCount ) {
CG_DamageFeedback( ps->damageYaw, ps->damagePitch, ps->damageCount );
}
// respawning / map restart
respawn = ps->persistant[PERS_SPAWN_COUNT] != ops->persistant[PERS_SPAWN_COUNT];
if ( respawn || cg.mapRestart ) {
cg.mapRestart = qfalse;
CG_Respawn();
}
if ( cg.snap->ps.pm_type != PM_INTERMISSION
&& ps->persistant[PERS_TEAM] != TEAM_SPECTATOR ) {
CG_CheckLocalSounds( ps, ops );
}
// check for going low on ammo
CG_CheckAmmo();
// try to play potentially dropped events
CG_PlayDroppedEvents( ps, ops );
// run events
CG_CheckPlayerstateEvents( ps, ops );
// reset event stack
eventStack = 0;
// smooth the ducking viewheight change
if ( ps->viewheight != ops->viewheight && !respawn ) {
cg.duckChange = ps->viewheight - ops->viewheight;
cg.duckTime = cg.time;
}
}

This would also allow to use the option even if the server doesn't support it.

Maybe I'm micro-optimizing here, but I think that we should expect the "damage" events to take up a significant portion of the traffic?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They don’t seem to be appreciably noisy in my testing, but that’s helped by aggregating damage during server frames and sending just one.

My initial try was to do exactly what you said re: estimating the damage client side. I ran into issues with things like splash damage, though, because the client doesn’t know the split of damage per player hit.

}

/*
============
AddScore
Expand Down Expand Up @@ -805,6 +826,8 @@ void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker,
int asave;
int knockback;
int max;
int i;
qboolean found;
#ifdef MISSIONPACK
vec3_t bouncedir, impactpoint;
#endif
Expand Down Expand Up @@ -1018,6 +1041,24 @@ void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker,
attacker->client->damage.amount += take + asave;
}
#endif
if ( !OnSameTeam( targ, attacker ) ) {
// accumulate damage per target for damage plums
found = qfalse;
for ( i = 0; i < attacker->client->damagePlumCount; i++ ) {
if ( attacker->client->damagePlums[i].clientNum == targ->s.number ) {
attacker->client->damagePlums[i].damage += take + asave;
found = qtrue;
break;
}
}
if ( !found && attacker->client->damagePlumCount < MAX_CLIENTS ) {
attacker->client->damagePlums[attacker->client->damagePlumCount].clientNum = targ->s.number;
attacker->client->damagePlums[attacker->client->damagePlumCount].damage = take + asave;
VectorCopy( targ->r.currentOrigin, attacker->client->damagePlums[attacker->client->damagePlumCount].origin );
attacker->client->damagePlums[attacker->client->damagePlumCount].origin[2] += 24;
attacker->client->damagePlumCount++;
}
}
}

// add to the damage inflicted on a player this frame
Expand Down
10 changes: 10 additions & 0 deletions code/game/g_local.h
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ typedef struct {
int teamVoted;

qboolean inGame;
qboolean damagePlums; // do we want to display damage numbers?
} clientPersistant_t;

// unlagged
Expand Down Expand Up @@ -334,6 +335,14 @@ struct gclient_s {
int enemy;
int amount;
} damage;

// damage plums - track damage per target for this frame
struct {
int clientNum;
int damage;
vec3_t origin;
} damagePlums[MAX_CLIENTS];
int damagePlumCount;
};


Expand Down Expand Up @@ -528,6 +537,7 @@ const char *BuildShaderStateConfig( void );
qboolean CanDamage (gentity_t *targ, vec3_t origin);
void G_Damage (gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, vec3_t dir, vec3_t point, int damage, int dflags, int mod);
qboolean G_RadiusDamage (vec3_t origin, gentity_t *attacker, float damage, float radius, gentity_t *ignore, int mod);
void DamagePlum( gentity_t *attacker, vec3_t origin, int damage );
int G_InvulnerabilityEffect( gentity_t *targ, vec3_t dir, vec3_t point, vec3_t impactpoint, vec3_t bouncedir );
void body_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath );
void TossClientItems( gentity_t *self );
Expand Down
1 change: 1 addition & 0 deletions code/game/g_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,7 @@ static void G_WarmupEnd( void )
client->ps.persistant[PERS_ATTACKER] = ENTITYNUM_NONE;
client->ps.persistant[PERS_ATTACKEE_ARMOR] = 0;
client->damage.enemy = client->damage.team = 0;
client->damagePlumCount = 0;

client->ps.stats[STAT_CLIENTS_READY] = 0;
client->ps.stats[STAT_HOLDABLE_ITEM] = 0;
Expand Down