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
45 changes: 45 additions & 0 deletions doc/action.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Additions and enhancements by darksaint, Reki, Rektek and the AQ2World team
- [Darkmatch](#darkmatch)
- [Map Restarting](#map-restarting)
- [Statistics](#statistics)
- [True Damage Tracking](#true-damage-tracking)
- [Automatic Joining/Equipping/Menu](#automatic-joiningequippingmenu)
- [Automatic Demo Recording](#automatic-demo-recording)
- [Spawn Code](#spawn-code)
Expand Down Expand Up @@ -782,6 +783,50 @@ The following read-only cvars are automatically maintained by the server and exp
- `t2` - current score for Team 2
- `t3` - current score for Team 3 (3-team modes)

### True Damage Tracking

Standard damage statistics count raw damage dealt, including overkill. A sniper headshot deals 450+ damage, but the target only had 100 HP — the "true" damage is 100. True damage tracking provides two things:

1. **Attacker-side `true_damage_dealt`**: Accumulated damage dealt to other players, capped at each victim's remaining HP at the time damage is applied. This is tracked alongside the existing raw `damage_dealt` stat and persists across respawns and reconnects (via ghost). The sum of all players' `true_damage_dealt` plus environmental damage equals the total HP lost across all victims in a round.

2. **Victim-side HP breakdown**: When a player dies, their 100 HP is categorized into three buckets that sum to exactly 100:
- **Player damage**: HP lost from instant hits by other players (kevlar bruise damage, kicks, punches)
- **Bleeding damage**: HP lost from bleed-over-time ticks (the primary way gunshot damage depletes HP in AQ2)
- **Environmental damage**: HP lost from falls, drowning, lava, slime, or self-inflicted damage

**Commands:**
- `truedmg` - displays the victim-side HP breakdown from the player's last death (client side)
- `stats` - now includes a "True Damage Dealt" line alongside the existing raw damage stat (client side)

**Scoreboard:**

True damage dealt can optionally be shown on the configurable scoreboard using the `E` field code. The `scoreboard` cvar accepts a string of field codes that define which columns appear:

| Code | Column | Width |
|------|--------|-------|
| `F` | Frags | 5 chars |
| `N` | Player name | 15 chars |
| `M` | Time (minutes) | 4 chars |
| `P` | Ping | 4 chars |
| `S` | Score | 5 chars |
| `K` | Kills | 5 chars |
| `D` | Deaths | 6 chars |
| `I` | Damage (raw) | 6 chars |
| `E` | TrDmg (true damage) | 6 chars |
| `A` | Accuracy (%) | 3 chars |
| `T` | Team | 4 chars |
| `C` | CTF Caps | 4 chars |

The `E` field is **not included in any default scoreboard layout** and must be explicitly added by the server operator. This avoids adding packet overhead for legacy clients or servers that don't need it. Each additional column adds approximately 7-8 bytes per player line to the layout string, which is constrained to 1024 bytes. Server operators should be mindful of the total column count when many players are on the server.

Example: `set scoreboard "FNMPEI"` adds true damage alongside raw damage on the scoreboard.

Default layouts (when `scoreboard` is empty):
- Standard teamplay: `FNMPIT`
- Team deathmatch: `FNMPDT`
- CTF: `SNMPCT`
- No-score mode: `NMP`

### Automatic Joining/Equipping/Menu
For the lazy players under us, we have created three new commands to make things easier.

Expand Down
1 change: 1 addition & 0 deletions src/action/a_cmds.c
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,7 @@ void Cmd_Ghost_f(edict_t * ent)
ent->client->resp.kills = ghost->kills;
ent->client->resp.deaths = ghost->deaths;
ent->client->resp.damage_dealt = ghost->damage_dealt;
ent->client->resp.true_damage_dealt = ghost->true_damage_dealt;
ent->client->resp.ctf_caps = ghost->ctf_caps;
ent->client->resp.ctf_capstreak = ghost->ctf_capstreak;
ent->client->resp.team_kills = ghost->team_kills;
Expand Down
5 changes: 5 additions & 0 deletions src/action/a_team.c
Original file line number Diff line number Diff line change
Expand Up @@ -1981,6 +1981,7 @@ void ResetScores (qboolean playerScores)
ent->client->resp.score = 0;
ent->client->resp.kills = 0;
ent->client->resp.damage_dealt = 0;
ent->client->resp.true_damage_dealt = 0;
ent->client->resp.streakHS = 0;
ent->client->resp.streakKills = 0;
ent->client->resp.roundStreakKills = 0;
Expand Down Expand Up @@ -3834,6 +3835,7 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer)
else if( field == 'K' ) chars += 5;
else if( field == 'D' ) chars += 6;
else if( field == 'I' ) chars += 6;
else if( field == 'E' ) chars += 6;
else if( field == 'A' ) chars += 3;
else chars ++;
}
Expand Down Expand Up @@ -3862,6 +3864,7 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer)
else if( field == 'K' ) strcat( string, "Kills" );
else if( field == 'D' ) strcat( string, "Deaths" );
else if( field == 'I' ) strcat( string, "Damage" );
else if( field == 'E' ) strcat( string, "TrDmg " );
else if( field == 'A' ) strcat( string, "Acc" );
else sprintf( string + strlen(string), "%c", sb[ i ] );
}
Expand All @@ -3884,6 +3887,7 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer)
else if( field == 'K' ) strcat( string, "\x9D\x9E\x9E\x9E\x9F" );
else if( field == 'D' ) strcat( string, "\x9D\x9E\x9E\x9E\x9E\x9F" );
else if( field == 'I' ) strcat( string, "\x9D\x9E\x9E\x9E\x9E\x9F" );
else if( field == 'E' ) strcat( string, "\x9D\x9E\x9E\x9E\x9E\x9F" );
else if( field == 'A' ) strcat( string, "\x9D\x9E\x9F" );
else sprintf( string + strlen(string), "%c", sb[ i ] );
}
Expand Down Expand Up @@ -3958,6 +3962,7 @@ void A_ScoreboardMessage (edict_t * ent, edict_t * killer)
else if( field == 'K' ) Q_snprintf( buf, sizeof(buf), "%5i", min( 99999, cl->resp.kills) );
else if( field == 'D' ) Q_snprintf( buf, sizeof(buf), "%6i", min( 999999, cl->resp.deaths) );
else if( field == 'I' ) Q_snprintf( buf, sizeof(buf), "%6i", min( 999999, cl->resp.damage_dealt) );
else if( field == 'E' ) Q_snprintf( buf, sizeof(buf), "%6i", min( 999999, cl->resp.true_damage_dealt) );
else if( field == 'A' ) Q_snprintf( buf, sizeof(buf), "%3.f", cl->resp.shotsTotal ? (double) cl->resp.hitsTotal * 100.0 / (double) cl->resp.shotsTotal : 0. );
else sprintf( buf, "%c", sb[ j ] );

Expand Down
5 changes: 5 additions & 0 deletions src/action/g_cmds.c
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,10 @@ static void Cmd_PrintStats_f (edict_t *ent) {
Cmd_Stats_f(ent, gi.argv(1));
}

static void Cmd_PrintTrueDmg_f (edict_t *ent) {
Cmd_TrueDmg_f(ent);
}

static void Cmd_Timeout_f (edict_t *ent) {
Cmd_CallTimeout_f(ent);
}
Expand Down Expand Up @@ -1994,6 +1998,7 @@ static cmdList_t commandList[] =
{ "unlock", Cmd_UnlockTeam_f, 0 },
{ "entcount", Cmd_Ent_Count_f, 0 },
{ "stats", Cmd_PrintStats_f, 0 },
{ "truedmg", Cmd_PrintTrueDmg_f, 0 },
{ "flashlight", FL_make, CMDF_PAUSE },
{ "matchadmin", Cmd_SetAdmin_f, 0 },
{ "roundtimeleft", Cmd_Roundtimeleft_f, 0 },
Expand Down
29 changes: 28 additions & 1 deletion src/action/g_combat.c
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,15 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve
else
SpawnDamage(te_sparks, point, normal, take);

// True damage tracking (victim-side): cap at remaining HP
if (instant_dam && client && take > 0 && targ->health > 0) {
int true_take = (take > targ->health) ? targ->health : take;
if (attacker->client && attacker != targ)
client->truedmg_player += true_take;
else
client->truedmg_env += true_take;
}

// all things that have at least some instantaneous damage, i.e. bruising/falling
if (instant_dam)
targ->health = targ->health - take;
Expand All @@ -869,6 +878,15 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve
attacker->client->resp.damage_dealt += damage;
// Hit markers
attacker->client->damage_dealt += damage;

// True damage dealt: only credit instant hits here.
// Bleeding damage is credited in Do_Bleeding when HP actually drops.
if (instant_dam) {
int pre_health = targ->health + take; // reconstruct pre-reduction HP
int true_take_att = (damage > pre_health) ? pre_health : damage;
if (true_take_att > 0)
attacker->client->resp.true_damage_dealt += true_take_att;
}
}
if (mod > 0 && mod < MAX_GUNSTAT) {
attacker->client->resp.gunstats[mod].damage += damage;
Expand All @@ -880,7 +898,7 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve
mod != MOD_TARGET_LASER) {
attacker->client->damage_dealt += take + psave + asave;
}

client->attacker = attacker;
client->attacker_mod = mod;
client->attacker_loc = damage_type;
Expand Down Expand Up @@ -936,6 +954,15 @@ void T_Damage (edict_t * targ, edict_t * inflictor, edict_t * attacker, const ve
attacker->client->resp.damage_dealt += damage;
// Hit markers
attacker->client->damage_dealt += damage;

// True damage dealt: only credit instant hits here.
// Bleeding damage is credited in Do_Bleeding when HP actually drops.
if (instant_dam) {
int victim_hp = targ->health + take; // reconstruct pre-reduction HP
int true_dmg = (damage > victim_hp) ? victim_hp : damage;
if (true_dmg > 0)
attacker->client->resp.true_damage_dealt += true_dmg;
}
}
// All normal weapon damage
if (mod > 0 && mod < MAX_GUNSTAT) {
Expand Down
12 changes: 12 additions & 0 deletions src/action/g_local.h
Original file line number Diff line number Diff line change
Expand Up @@ -2022,6 +2022,12 @@ typedef struct
int deaths; // deaths

int damage_dealt; // keep track of damage dealt by player to other players
int true_damage_dealt; // damage dealt capped at victim's remaining HP

// Last life's true damage breakdown (for truedmg command, persists across respawn)
int last_truedmg_player; // HP lost from other players' instant hits
int last_truedmg_bleed; // HP lost from bleeding ticks
int last_truedmg_env; // HP lost from environment/self damage

int team; // team the player is on
int subteam;
Expand Down Expand Up @@ -2178,6 +2184,11 @@ struct gclient_s
vec3_t damage_from; // origin for vector calculation
int damage_dealt; // total damage dealt to other players (used for hit markers)

// True damage breakdown (victim-side, per-life, sums to 100 on death)
int truedmg_player; // HP lost from other players' instant hits
int truedmg_bleed; // HP lost from bleeding ticks
int truedmg_env; // HP lost from environment/self damage

float killer_yaw; // when dead, look at killer

weaponstate_t weaponstate;
Expand Down Expand Up @@ -2870,6 +2881,7 @@ typedef struct
int kills;
int deaths;
int damage_dealt;
int true_damage_dealt;
int ctf_caps;
int shotsTotal;
int hitsTotal;
Expand Down
7 changes: 7 additions & 0 deletions src/action/p_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -1860,6 +1860,12 @@ void player_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage
}
LookAtKiller(self, inflictor, attacker);
self->client->ps.pmove.pm_type = PM_DEAD;

// Snapshot true damage breakdown for truedmg command
self->client->resp.last_truedmg_player = self->client->truedmg_player;
self->client->resp.last_truedmg_bleed = self->client->truedmg_bleed;
self->client->resp.last_truedmg_env = self->client->truedmg_env;

ClientObituary(self, inflictor, attacker);
if (ctf->value)
CTFFragBonuses(self, inflictor, attacker);
Expand Down Expand Up @@ -4040,6 +4046,7 @@ void CreateGhost(edict_t * ent)
// Score
ghost->score = ent->client->resp.score;
ghost->damage_dealt = ent->client->resp.damage_dealt;
ghost->true_damage_dealt = ent->client->resp.true_damage_dealt;
ghost->kills = ent->client->resp.kills;
ghost->deaths = ent->client->resp.deaths;
ghost->ctf_caps = ent->client->resp.ctf_caps;
Expand Down
13 changes: 12 additions & 1 deletion src/action/p_view.c
Original file line number Diff line number Diff line change
Expand Up @@ -1203,11 +1203,22 @@ void Do_Bleeding (edict_t * ent)
damage = (int) (ent->client->bleed_remain / BLEED_TIME);
if (ent->client->bleed_remain >= BLEED_TIME)
{
// True damage tracking: cap bleed at remaining HP
if (damage > 0 && ent->health > 0) {
int true_bleed = (damage > ent->health) ? ent->health : damage;
ent->client->truedmg_bleed += true_bleed;

// Credit the attacker's true_damage_dealt when HP actually drops
if (ent->client->attacker && ent->client->attacker->client
&& ent->client->attacker != ent)
ent->client->attacker->client->resp.true_damage_dealt += true_bleed;
}

ent->health -= damage;
if (damage > 1)
{
// action doens't do this
//ent->client->damage_blood += damage; // for feedback
//ent->client->damage_blood += damage; // for feedback
}
if (ent->health <= 0)
{
Expand Down
16 changes: 16 additions & 0 deletions src/action/tng_stats.c
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,21 @@ void Cmd_Stats_f (edict_t *targetent, char *arg)
gi.cprintf (targetent, PRINT_HIGH, "Average Accuracy: %.2f\n", perc_hit); // Average
gi.cprintf (targetent, PRINT_HIGH, "\n\x9D\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9E\x9F\n\n");
gi.cprintf(targetent, PRINT_HIGH, "Highest streaks: kills: %d headshots: %d\n", ent->client->resp.streakKillsHighest, ent->client->resp.streakHSHighest);
gi.cprintf(targetent, PRINT_HIGH, "True Damage Dealt: %d (Raw: %d)\n",
ent->client->resp.true_damage_dealt, ent->client->resp.damage_dealt);
}

void Cmd_TrueDmg_f(edict_t *ent)
{
int p = ent->client->resp.last_truedmg_player;
int b = ent->client->resp.last_truedmg_bleed;
int e = ent->client->resp.last_truedmg_env;
if (p + b + e == 0) {
gi.cprintf(ent, PRINT_HIGH, "No true damage data yet.\n");
return;
}
gi.cprintf(ent, PRINT_HIGH, "Last life HP breakdown: %d player, %d bleeding, %d environment (total: %d)\n",
p, b, e, p + b + e);
}

void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer)
Expand Down Expand Up @@ -1592,6 +1607,7 @@ void LogEndMatchStats(void)
ghlient->resp.kills = ghost->kills;
ghlient->resp.deaths = ghost->deaths;
ghlient->resp.damage_dealt = ghost->damage_dealt;
ghlient->resp.true_damage_dealt = ghost->true_damage_dealt;
ghlient->resp.ctf_caps = ghost->ctf_caps;
ghlient->resp.ctf_capstreak = ghost->ctf_capstreak;
ghlient->resp.team_kills = ghost->team_kills;
Expand Down
1 change: 1 addition & 0 deletions src/action/tng_stats.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ void Stats_AddHit(edict_t *ent, int gun, int hitPart);
float CalculateAccuracy(edict_t* ent);
void A_ScoreboardEndLevel (edict_t * ent, edict_t * killer);
void Cmd_Stats_f (edict_t *targetent, char *arg);
void Cmd_TrueDmg_f(edict_t *ent);
void Cmd_Statmode_f(edict_t *ent);
edict_t *find_player_by_steamid(const char* steamid);
Loading