Skip to content

Shotgun doesn't gib (except when aiming at the feet) #794

@WofWca

Description

@WofWca
shotgun_body_no_gib.mp4
shotgun_feet_gib.mp4

Reproduction steps:

  1. devmap q3dm1
  2. addbot random 1 x5
  3. bot_pause 1
  4. give all
  5. give quad damage
  6. g_debugDamage 1
  7. Try to shoot with the shotgun at either feet or just the body.

How the code works

Here is the weapon_supershotgun_fire function:

ioq3/code/game/g_weapon.c

Lines 349 to 360 in f976711

void weapon_supershotgun_fire (gentity_t *ent) {
gentity_t *tent;
// send shotgun blast
tent = G_TempEntity( muzzle, EV_SHOTGUN );
VectorScale( forward, 4096, tent->s.origin2 );
SnapVector( tent->s.origin2 );
tent->s.eventParm = rand() & 255; // seed for spread pattern
tent->s.otherEntityNum = ent->s.number;
ShotgunPattern( tent->s.pos.trBase, tent->s.origin2, tent->s.eventParm, ent );
}

It creates 11 pellets, and runs ShotgunPellet for each one, separately, one by one, in a loop. Each ShotgunPellet then potentially calls G_Damage, which, in turn, is responsible for calling targ->die:

ioq3/code/game/g_combat.c

Lines 803 to 1048 in f976711

void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker,
vec3_t dir, vec3_t point, int damage, int dflags, int mod ) {
gclient_t *client;
int take;
int asave;
int knockback;
int max;
#ifdef MISSIONPACK
vec3_t bouncedir, impactpoint;
#endif
if (!targ->takedamage) {
return;
}
// the intermission has already been qualified for, so don't
// allow any extra scoring
if ( level.intermissionQueued ) {
return;
}
#ifdef MISSIONPACK
if ( targ->client && mod != MOD_JUICED) {
if ( targ->client->invulnerabilityTime > level.time) {
if ( dir && point ) {
G_InvulnerabilityEffect( targ, dir, point, impactpoint, bouncedir );
}
return;
}
}
#endif
if ( !inflictor ) {
inflictor = &g_entities[ENTITYNUM_WORLD];
}
if ( !attacker ) {
attacker = &g_entities[ENTITYNUM_WORLD];
}
// shootable doors / buttons don't actually have any health
if ( targ->s.eType == ET_MOVER ) {
if ( targ->use && targ->moverState == MOVER_POS1 ) {
targ->use( targ, inflictor, attacker );
}
return;
}
#ifdef MISSIONPACK
if( g_gametype.integer == GT_OBELISK && CheckObeliskAttack( targ, attacker ) ) {
return;
}
#endif
// reduce damage by the attacker's handicap value
// unless they are rocket jumping
if ( attacker->client && attacker != targ ) {
max = attacker->client->ps.stats[STAT_MAX_HEALTH];
#ifdef MISSIONPACK
if( bg_itemlist[attacker->client->ps.stats[STAT_PERSISTANT_POWERUP]].giTag == PW_GUARD ) {
max /= 2;
}
#endif
damage = damage * max / 100;
}
client = targ->client;
if ( client ) {
if ( client->noclip ) {
return;
}
}
if ( !dir ) {
dflags |= DAMAGE_NO_KNOCKBACK;
} else {
VectorNormalize(dir);
}
knockback = damage;
if ( knockback > 200 ) {
knockback = 200;
}
if ( targ->flags & FL_NO_KNOCKBACK ) {
knockback = 0;
}
if ( dflags & DAMAGE_NO_KNOCKBACK ) {
knockback = 0;
}
// figure momentum add, even if the damage won't be taken
if ( knockback && targ->client ) {
vec3_t kvel;
float mass;
mass = 200;
VectorScale (dir, g_knockback.value * (float)knockback / mass, kvel);
VectorAdd (targ->client->ps.velocity, kvel, targ->client->ps.velocity);
// set the timer so that the other client can't cancel
// out the movement immediately
if ( !targ->client->ps.pm_time ) {
int t;
t = knockback * 2;
if ( t < 50 ) {
t = 50;
}
if ( t > 200 ) {
t = 200;
}
targ->client->ps.pm_time = t;
targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
}
}
// check for completely getting out of the damage
if ( !(dflags & DAMAGE_NO_PROTECTION) ) {
// if TF_NO_FRIENDLY_FIRE is set, don't do damage to the target
// if the attacker was on the same team
#ifdef MISSIONPACK
if ( mod != MOD_JUICED && targ != attacker && !(dflags & DAMAGE_NO_TEAM_PROTECTION) && OnSameTeam (targ, attacker) ) {
#else
if ( targ != attacker && OnSameTeam (targ, attacker) ) {
#endif
if ( !g_friendlyFire.integer ) {
return;
}
}
#ifdef MISSIONPACK
if (mod == MOD_PROXIMITY_MINE) {
if (inflictor && inflictor->parent && OnSameTeam(targ, inflictor->parent)) {
return;
}
if (targ == attacker) {
return;
}
}
#endif
// check for godmode
if ( targ->flags & FL_GODMODE ) {
return;
}
}
// battlesuit protects from all radius damage (but takes knockback)
// and protects 50% against all damage
if ( client && client->ps.powerups[PW_BATTLESUIT] ) {
G_AddEvent( targ, EV_POWERUP_BATTLESUIT, 0 );
if ( ( dflags & DAMAGE_RADIUS ) || ( mod == MOD_FALLING ) ) {
return;
}
damage *= 0.5;
}
// add to the attacker's hit counter (if the target isn't a general entity like a prox mine)
if ( attacker->client && client
&& targ != attacker && targ->health > 0
&& targ->s.eType != ET_MISSILE
&& targ->s.eType != ET_GENERAL) {
if ( OnSameTeam( targ, attacker ) ) {
attacker->client->ps.persistant[PERS_HITS]--;
} else {
attacker->client->ps.persistant[PERS_HITS]++;
}
attacker->client->ps.persistant[PERS_ATTACKEE_ARMOR] = (targ->health<<8)|(client->ps.stats[STAT_ARMOR]);
}
// always give half damage if hurting self
// calculated after knockback, so rocket jumping works
if ( targ == attacker) {
damage *= 0.5;
}
if ( damage < 1 ) {
damage = 1;
}
take = damage;
// save some from armor
asave = CheckArmor (targ, take, dflags);
take -= asave;
if ( g_debugDamage.integer ) {
G_Printf( "%i: client:%i health:%i damage:%i armor:%i\n", level.time, targ->s.number,
targ->health, take, asave );
}
// add to the damage inflicted on a player this frame
// the total will be turned into screen blends and view angle kicks
// at the end of the frame
if ( client ) {
if ( attacker ) {
client->ps.persistant[PERS_ATTACKER] = attacker->s.number;
} else {
client->ps.persistant[PERS_ATTACKER] = ENTITYNUM_WORLD;
}
client->damage_armor += asave;
client->damage_blood += take;
client->damage_knockback += knockback;
if ( dir ) {
VectorCopy ( dir, client->damage_from );
client->damage_fromWorld = qfalse;
} else {
VectorCopy ( targ->r.currentOrigin, client->damage_from );
client->damage_fromWorld = qtrue;
}
}
// See if it's the player hurting the emeny flag carrier
#ifdef MISSIONPACK
if( g_gametype.integer == GT_CTF || g_gametype.integer == GT_1FCTF ) {
#else
if( g_gametype.integer == GT_CTF) {
#endif
Team_CheckHurtCarrier(targ, attacker);
}
if (targ->client) {
// set the last client who damaged the target
targ->client->lasthurt_client = attacker->s.number;
targ->client->lasthurt_mod = mod;
}
// do the damage
if (take) {
targ->health = targ->health - take;
if ( targ->client ) {
targ->client->ps.stats[STAT_HEALTH] = targ->health;
}
if ( targ->health <= 0 ) {
if ( client )
targ->flags |= FL_NO_KNOCKBACK;
if (targ->health < -999)
targ->health = -999;
targ->enemy = attacker;
targ->die (targ, inflictor, attacker, take, mod);
return;
} else if ( targ->pain ) {
targ->pain (targ, attacker, take);
}
}
}

That is, if the target is at 15 health, it will only "absorb" 2 pellets (which deal 10 damage each), and then transition into the body (dead body) state. Other pellets will do their thing as if the player is already dead.

The problem

The problem is that it seem that the dead body actually has a shorter hitbox than the player before it died. And this is what results in the player getting gibbed only if you're aiming at the feet.

I've always been annoyed at how "weak" the shotgun feels.

Misc

This issue is also present in the original (Steam) version of Quake, the demo version of Quake 1.11, and OpenArena.
Edit: and in Quake Live.

This issue is not present in the Excessive Plus mod. But, as I figured out, this is because dead bodies' hitboxes actually are as tall as players. This can be verified by killing someone with the machine gun while aiming at their head, and then continuing shooting. You'll see blood marks, and you'll gib the body, which is not the case in vanilla Quake and ioquake3.

The problem actually is not just gibbing, but also knockback of the shotgun, which is much weaker than that of, let's say railgun. Try shooting at a bot with a shotgun point blank, without killing it, and compare it with the knockback from the railgun.
While this might be considered "an intended gameplay mechanic", I really think that at the gibbing should be fixed. Firstly to make the shotgun much more satisfying, and secondly: more consistent (things should not depend on whether you aim at the feet).

Potential fixes

  • Calculate gibbing right on kill event. Hmm, might be too much to rework?
    Maybe make ShotgunPellet not call G_Damage immediately, instead, in the caller of ShotgunPellet, which is ShotgunPattern, make an array of entities which receive damage (there can be multiple of them). Then, bin each pellet into the entity that takes damage, and call G_Damage for each entity, with the sum of all pellets that they took.
    Downside: it's not possible to kill a player through another player, because the first player will take all of the damage
  • Don't turn the player into a corpse immediately, keep them until the end of the frame so that all the bullets go into the player.
    Downsides: same as above.
  • For just one frame, make the corpse's hitbox equivalent to the player's hitbox.
    Downsides: same as above, but better, because only some of the bullets will hit the first player's body, until it's gibbed. The rest will still reach the other player standing behind the first one.

And let's examine another option, that I am not sure why it's working

My fix

While playing around with the code, I found this line:

self->r.maxs[2] = -8;

This line has been there from the very release of the source code. Initially I thought that this is what sets the height of the dead body, so simply removing it will not be good.

Buuut, weirdly, it seems to have worked! I removed this line, and shotgun gibbing started working, while still keeping the heights of dead bodies!

shotgun_gib_fixed.mp4
shotgun_gib_fixed_body_hitbox.mp4

As I mentioned, however, I am not entirely sure as to what the purpose of the line is then. Maybe it's actually intentional, to make sure that you can shoot through people when they die. But then I don't see why it would set r.maxs[2] to exactly -8. If you put a log entry there, you will notice that this doesn't make the hitbox completely disappear. It only gets a little shorter.
Or maybe it's just some leftover code that is actually not needed. Maybe it used to set the camera position to get low when you die. Maybe it used to set the dead body height (as mentioned above), but is no longer needed.
I found the same line in PM_CheckDuck, which is what seems to be responsible for camera position and (I'm not sure) the height of the dead body.

ioq3/code/game/bg_pmove.c

Lines 1276 to 1281 in bad8c3b

if (pm->ps->pm_type == PM_DEAD)
{
pm->maxs[2] = -8;
pm->ps->viewheight = DEAD_VIEWHEIGHT;
return;
}

So, maybe we can remove the same line from player_die after all?
Anyways, here is the MR: #795.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions