-
-
Notifications
You must be signed in to change notification settings - Fork 566
Description
shotgun_body_no_gib.mp4
shotgun_feet_gib.mp4
Reproduction steps:
devmap q3dm1
addbot random 1
x5bot_pause 1
give all
give quad damage
g_debugDamage 1
- Try to shoot with the shotgun at either feet or just the body.
How the code works
Here is the weapon_supershotgun_fire
function:
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
:
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 makeShotgunPellet
not callG_Damage
immediately, instead, in the caller ofShotgunPellet
, which isShotgunPattern
, 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 callG_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:
Line 604 in f976711
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.
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.