diff --git a/README.md b/README.md index 98a2c9c1..23309b1e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,13 @@ Unofficial Quake III Arena gamecode patch * fixed UI mouse sensitivity for high-resolution * fixed not being able to gib after match end (right before showing the scores) * fixed shotgun not gibbing unless aiming at the feet + * improved gibs physics, new CVARs + * `cg_oldGibs` + * `cg_gibs` is now non-binary: set to 1.3 to launch 3 more pieces of gibs, + or to 0.5 to half the amount of gibs + * `cg_gibsInheritPlayerVelocity` + * `cg_gibsExtraRandomVelocity` + * `cg_gibsExtraVerticalVelocity` * fixed server browser + faster scanning * fixed grappling hook muzzle position visuals * new demo UI (subfolders,filtering,sorting) diff --git a/code/cgame/cg_cvar.h b/code/cgame/cg_cvar.h index 46811af9..16120f6e 100644 --- a/code/cgame/cg_cvar.h +++ b/code/cgame/cg_cvar.h @@ -17,7 +17,14 @@ CG_CVAR( cg_zoomFov, "cg_zoomfov", "22.5", CVAR_ARCHIVE ) CG_CVAR( cg_fov, "cg_fov", "90", CVAR_ARCHIVE ) CG_CVAR( cg_viewsize, "cg_viewsize", "100", CVAR_ARCHIVE ) CG_CVAR( cg_shadows, "cg_shadows", "1", CVAR_ARCHIVE ) -CG_CVAR( cg_gibs, "cg_gibs", "1", CVAR_ARCHIVE ) +CG_CVAR( cg_gibs, "cg_gibs", "1.0", CVAR_ARCHIVE ) +CG_CVAR( cg_oldGibs, "cg_oldGibs", "0", CVAR_ARCHIVE ) +CG_CVAR( cg_gibsInheritPlayerVelocity, "cg_gibsInheritPlayerVelocity", "1.0", CVAR_ARCHIVE ) +CG_CVAR( cg_gibsRandomVelocityFromKnockback, "cg_gibsRandomVelocityFromKnockback", "0.15", CVAR_ARCHIVE ) +CG_CVAR( cg_gibsExtraRandomVelocity, "cg_gibsExtraRandomVelocity", "175", CVAR_ARCHIVE ) +CG_CVAR( cg_gibsExtraVerticalVelocity, "cg_gibsExtraVerticalVelocity", "100", CVAR_ARCHIVE ) +CG_CVAR( cg_gibsBounceFactor, "cg_gibsBounceFactor", "0.4", CVAR_ARCHIVE ) +CG_CVAR( cg_gibsRotationFactor, "cg_gibsRotationFactor", "1.0", CVAR_ARCHIVE ) CG_CVAR( cg_draw2D, "cg_draw2D", "1", CVAR_ARCHIVE ) CG_CVAR( cg_drawStatus, "cg_drawStatus", "1", CVAR_ARCHIVE ) CG_CVAR( cg_drawTimer, "cg_drawTimer", "0", CVAR_ARCHIVE ) @@ -39,6 +46,9 @@ CG_CVAR( cg_crosshairY, "cg_crosshairY", "0", CVAR_ARCHIVE ) CG_CVAR( cg_brassTime, "cg_brassTime", "2500", CVAR_ARCHIVE ) CG_CVAR( cg_simpleItems, "cg_simpleItems", "0", CVAR_ARCHIVE ) CG_CVAR( cg_addMarks, "cg_marks", "1", CVAR_ARCHIVE ) +// Note that ~290 corresponds to a free fall with no bounce from player height. +CG_CVAR( cg_bounceMarksMinImpactSpeed, "cg_bounceMarksMinImpactSpeed", "350", CVAR_ARCHIVE ) +CG_CVAR( cg_bounceSoundMinImpactSpeed, "cg_bounceSoundMinImpactSpeed", "450", CVAR_ARCHIVE ) CG_CVAR( cg_lagometer, "cg_lagometer", "1", CVAR_ARCHIVE ) CG_CVAR( cg_railTrailTime, "cg_railTrailTime", "400", CVAR_ARCHIVE ) CG_CVAR( cg_railTrailRadius, "cg_railTrailRadius", "0", CVAR_ARCHIVE ) diff --git a/code/cgame/cg_effects.c b/code/cgame/cg_effects.c index 115c309f..d5f9ef01 100644 --- a/code/cgame/cg_effects.c +++ b/code/cgame/cg_effects.c @@ -531,9 +531,13 @@ void CG_Bleed( const vec3_t origin, int entityNum ) { /* ================== CG_LaunchGib + +`randSeed` has no effect if `cg_oldGibs.integer == 1` ================== */ -static void CG_LaunchGib( const vec3_t origin, const vec3_t velocity, qhandle_t hModel ) { +static void CG_LaunchGib( const vec3_t origin, const vec3_t angles, + const vec3_t velocity, qhandle_t hModel, + const int randSeed ) { localEntity_t *le; refEntity_t *re; @@ -545,7 +549,7 @@ static void CG_LaunchGib( const vec3_t origin, const vec3_t velocity, qhandle_t le->endTime = le->startTime + 5000 + random() * 3000; VectorCopy( origin, re->origin ); - AxisCopy( axisDefault, re->axis ); + AnglesToAxis( angles, re->axis ); re->hModel = hModel; le->pos.trType = TR_GRAVITY; @@ -553,36 +557,324 @@ static void CG_LaunchGib( const vec3_t origin, const vec3_t velocity, qhandle_t VectorCopy( velocity, le->pos.trDelta ); le->pos.trTime = cg.time; - le->bounceFactor = 0.6f; + le->bounceFactor = cg_oldGibs.integer ? 0.6f : cg_gibsBounceFactor.value; + + if (!cg_oldGibs.integer) { + // `VectorLength` would be more precise, but this is faster + // and good enough for randomness. + float speedIsh = fabs(velocity[0]) + fabs(velocity[1]) + fabs(velocity[2]); + int i; + int seed = randSeed; + int mainRotationAxis = Q_rand(&seed) % 3; + + le->leFlags = LEF_TUMBLE; + le->angles.trType = TR_LINEAR; + le->angles.trTime = cg.time; + VectorCopy( angles, le->angles.trBase ); + // Just a few degrees of randomness. + le->angles.trBase[PITCH] += Q_rand(&seed)&7; + le->angles.trBase[YAW] += Q_rand(&seed)&7; + le->angles.trBase[ROLL] += Q_rand(&seed)&7; + // TODO the tumble speed should probably depend on damage instead, + // or at least on random velocity. + for ( i = 0; i < 3; i++ ) { + // The numbers are not based on science, but it looks like + // having one axis be bigger than others makes rotation look natural. + float axisMul = mainRotationAxis == i ? 1 : 0.25; + le->angles.trDelta[i] = speedIsh * axisMul * 0.5 * + cg_gibsRotationFactor.value * Q_crandom(&seed); + } + } le->leBounceSoundType = LEBS_BLOOD; le->leMarkType = LEMT_BLOOD; } +#define PLAYER_RADIUS 15 +// If it's a dead body playing a death animation, +// gradually transition the body position and angles from upright +// to "lying flat on the ground". +void AdjustPositionIfDeathAnimation( const lerpFrame_t *anim, vec3_t origin, + vec3_t bodyAngles, vec3_t lookDirAngles ) { + // 0 means that the body is fully erect, + // 1 means it's lying flat on the ground. + float deathAnimationProgress = 0; + if ( + // Is this a death / dead animation? + (anim->animationNumber & ~ANIM_TOGGLEBIT) <= BOTH_DEAD3 && + (anim->animationNumber & ~ANIM_TOGGLEBIT) >= BOTH_DEATH1 && + // More sanity checks + anim->animation && + anim->animation->numFrames > 0 + ) { + const int frameOfAnimation = anim->frame - anim->animation->firstFrame; + if ( + frameOfAnimation < 0 || + frameOfAnimation >= anim->animation->numFrames + ) { + // Out of range. This seems to happen + // when we haven't yet managed to start the death animation. + // Maybe we're looking at the wrong things, + // but this works fine. + deathAnimationProgress = 0; + } else { + deathAnimationProgress = + (float)(frameOfAnimation + 1) / anim->animation->numFrames; + } + } + + // TODO fix: with body sinking, gibs get stuck in the floor. + origin[2] += deathAnimationProgress * (MINS_Z + PLAYER_RADIUS / 1.8f); + // From upright to facing up. + // TODO fix: but sometimes the "dead" animation is such that + // the player is facing down. + bodyAngles[PITCH] = 360 - deathAnimationProgress * 90; + lookDirAngles[PITCH] += - deathAnimationProgress * 90; + // Normalize. Doesn't seem to be necessary, but let's do it. + if (lookDirAngles[PITCH] < 0) { + lookDirAngles[PITCH] += 360; + } +} /* =================== CG_GibPlayer Generated a bunch of gibs launching out from the bodies location + +`randSeed` should be the same for all players, and also preserved +in demo playback, so that players see the same gibs +(as long as they have the same `cg_gibs*` CVAR values). =================== */ -#define GIB_VELOCITY 250 -#define GIB_JUMP 250 -void CG_GibPlayer( const vec3_t playerOrigin ) { - vec3_t origin, velocity; +#define DEFAULT_NUM_GIBS 10 +#define GIB_VELOCITY 250 +#define GIB_JUMP 250 +void CG_GibPlayer( const vec3_t playerOrigin, const vec3_t playerAngles, + const vec3_t playerVelocity, const int knockbackSpeed, + const lerpFrame_t *bodyAnimation, const int randSeed ) { + vec3_t baseOrigin, origin, velocity; + // Generally only the head should have pitch, + // the rest of the body is upright. + vec3_t bodyAngles; + vec3_t lookDirAngles, angles; + vec3_t forward, right, up; + // See `playerMins`, `playerMaxs`. + // TODO we could try to check the actual `mins` and `maxs` + // (do we have them available on the client though?), + // to account for crounching. + float playerHeight = 32 - MINS_Z; + float playerRadius = PLAYER_RADIUS; + float baseRandomVelocity = + cg_gibsExtraRandomVelocity.value + + cg_gibsRandomVelocityFromKnockback.value * knockbackSpeed; + int seed = randSeed; + + vec3_t playerVelocityScaled; + float jump = cg_gibsExtraVerticalVelocity.value; + int numGibs = cg_gibs.value * DEFAULT_NUM_GIBS; + qboolean skullLaunched = qfalse; // launch only one skull. + + if ( !cg_blood.integer ) { + return; + } + + VectorCopy( playerOrigin, baseOrigin ); + VectorCopy( playerAngles, lookDirAngles ); + VectorCopy( playerAngles, bodyAngles ); + if ( bodyAnimation ) { + AdjustPositionIfDeathAnimation( bodyAnimation, baseOrigin, bodyAngles, lookDirAngles ); + } else { + bodyAngles[PITCH] = 0; + } + AngleVectors( bodyAngles, forward, right, up ); + + VectorScale( playerVelocity, cg_gibsInheritPlayerVelocity.value, playerVelocityScaled ); + + do { + // Note that one gib will get launched even if `numGibs == 0`. + // This is in line with the original behavior of `CG_GibPlayer`. + + VectorCopy( baseOrigin, origin ); + VectorMA(origin, MINS_Z + 0.95 * playerHeight, up, origin); + VectorClear( velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, forward, velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, right, velocity ); + // For the skull / brain we want the random velocity + // to never have downwards (inwards) component, + // so we use `Q_random` instead of `Q_crandom`. + // We also do the same for other gibs, + // but for the left / right velocity components. + VectorMA( velocity, Q_random(&seed)*baseRandomVelocity, up, velocity ); + velocity[2] += jump; + VectorAdd( velocity, playerVelocityScaled, velocity ); + if ( !skullLaunched && (Q_rand(&seed) & 1) ) { + CG_LaunchGib( origin, lookDirAngles, velocity, cgs.media.gibSkull, Q_rand(&seed) ); + skullLaunched = qtrue; + } else { + CG_LaunchGib( origin, lookDirAngles, velocity, cgs.media.gibBrain, Q_rand(&seed) ); + } + if (--numGibs <= 0) { + return; + } + + VectorCopy( baseOrigin, origin ); + VectorMA( origin, MINS_Z + 0.65 * playerHeight, up, origin ); + VectorClear( velocity ); + velocity[0] = Q_crandom(&seed)*baseRandomVelocity; + velocity[1] = Q_crandom(&seed)*baseRandomVelocity; + velocity[2] = jump + Q_crandom(&seed)*baseRandomVelocity; + VectorAdd( velocity, playerVelocityScaled, velocity ); + CG_LaunchGib( origin, bodyAngles, velocity, cgs.media.gibAbdomen, Q_rand(&seed) ); + if (--numGibs <= 0) { + return; + } + + VectorCopy( baseOrigin, origin ); + VectorMA( origin, MINS_Z + 0.78 * playerHeight, up, origin ); + VectorMA( origin, 0.8 * playerRadius, right, origin ); + VectorMA( origin, -0.3 * playerRadius, forward, origin ); + VectorClear( velocity ); + VectorMA( velocity, +Q_random(&seed)*baseRandomVelocity, right, velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, forward, velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, up, velocity ); + velocity[2] += jump; + VectorAdd( velocity, playerVelocityScaled, velocity ); + VectorCopy( bodyAngles, angles ); + angles[ROLL] += 70; + angles[PITCH] += 45; + CG_LaunchGib( origin, angles, velocity, cgs.media.gibArm, Q_rand(&seed) ); + if (--numGibs <= 0) { + return; + } + + VectorCopy( baseOrigin, origin ); + VectorMA( origin, MINS_Z + 0.80 * playerHeight, up, origin ); + VectorClear( velocity ); + // Chest is a more "central" and "heavier" piece, + // so it gets less random velocity. + velocity[0] = 0.5*Q_crandom(&seed)*baseRandomVelocity; + velocity[1] = 0.5*Q_crandom(&seed)*baseRandomVelocity; + velocity[2] = jump + 0.5*Q_crandom(&seed)*baseRandomVelocity; + VectorAdd( velocity, playerVelocityScaled, velocity ); + CG_LaunchGib( origin, bodyAngles, velocity, cgs.media.gibChest, Q_rand(&seed) ); + if (--numGibs <= 0) { + return; + } + + VectorCopy( baseOrigin, origin ); + VectorMA( origin, MINS_Z + 0.66 * playerHeight, up, origin ); + VectorMA( origin, 0.8 * playerRadius, right, origin ); + VectorMA( origin, 0.2 * playerRadius, forward, origin ); + VectorClear( velocity ); + velocity[0] = Q_crandom(&seed)*baseRandomVelocity; + velocity[1] = Q_crandom(&seed)*baseRandomVelocity; + velocity[2] = jump + Q_crandom(&seed)*baseRandomVelocity; + VectorAdd( velocity, playerVelocityScaled, velocity ); + VectorCopy( bodyAngles, angles ); + angles[PITCH] -= 80; + angles[YAW] += 50; + CG_LaunchGib( origin, angles, velocity, cgs.media.gibFist, Q_rand(&seed) ); + if (--numGibs <= 0) { + return; + } + + VectorCopy( baseOrigin, origin ); + VectorMA( origin, MINS_Z + 0.05 * playerHeight, up, origin ); + VectorMA( origin, -0.5 * playerRadius, right, origin ); + VectorMA( origin, -0.5 * playerRadius, forward, origin ); + VectorClear( velocity ); + velocity[0] = Q_crandom(&seed)*baseRandomVelocity; + velocity[1] = Q_crandom(&seed)*baseRandomVelocity; + velocity[2] = jump + Q_crandom(&seed)*baseRandomVelocity; + VectorAdd( velocity, playerVelocityScaled, velocity ); + CG_LaunchGib( origin, bodyAngles, velocity, cgs.media.gibFoot, Q_rand(&seed) ); + if (--numGibs <= 0) { + return; + } + + VectorCopy( baseOrigin, origin ); + VectorMA( origin, MINS_Z + 0.65 * playerHeight, up, origin ); + VectorMA( origin, -0.6 * playerRadius, right, origin ); + VectorMA( origin, +0.2 * playerRadius, forward, origin ); + VectorClear( velocity ); + VectorMA( velocity, -Q_random(&seed)*baseRandomVelocity, right, velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, forward, velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, up, velocity ); + velocity[2] += jump; + VectorAdd( velocity, playerVelocityScaled, velocity ); + VectorCopy( bodyAngles, angles ); + angles[ROLL] -= 90; + angles[PITCH] -= 75; + CG_LaunchGib( origin, angles, velocity, cgs.media.gibForearm, Q_rand(&seed) ); + if (--numGibs <= 0) { + return; + } + + VectorCopy( baseOrigin, origin ); + VectorMA( origin, MINS_Z + 0.57 * playerHeight, up, origin ); + VectorClear( velocity ); + velocity[0] = Q_crandom(&seed)*baseRandomVelocity; + velocity[1] = Q_crandom(&seed)*baseRandomVelocity; + velocity[2] = jump + Q_crandom(&seed)*baseRandomVelocity; + VectorAdd( velocity, playerVelocityScaled, velocity ); + CG_LaunchGib( origin, bodyAngles, velocity, cgs.media.gibIntestine, Q_rand(&seed) ); + if (--numGibs <= 0) { + return; + } + + VectorCopy( baseOrigin, origin ); + VectorMA( origin, MINS_Z + 0.42 * playerHeight, up, origin ); + VectorMA( origin, 0.5 * playerRadius, right, origin ); + VectorMA( origin, 0.1 * playerRadius, forward, origin ); + VectorClear( velocity ); + VectorMA( velocity, +Q_random(&seed)*baseRandomVelocity, right, velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, forward, velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, up, velocity ); + velocity[2] += jump; + VectorAdd( velocity, playerVelocityScaled, velocity ); + VectorCopy( bodyAngles, angles ); + angles[ROLL] -= 30; + angles[PITCH] -= 15; + CG_LaunchGib( origin, angles, velocity, cgs.media.gibLeg, Q_rand(&seed) ); + if (--numGibs <= 0) { + return; + } + + VectorCopy( baseOrigin, origin ); + VectorMA( origin, MINS_Z + 0.44 * playerHeight, up, origin ); + VectorMA( origin, -0.5 * playerRadius, right, origin ); + VectorMA( origin, -0.2 * playerRadius, forward, origin ); + VectorClear( velocity ); + VectorMA( velocity, -Q_random(&seed)*baseRandomVelocity, right, velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, forward, velocity ); + VectorMA( velocity, Q_crandom(&seed)*baseRandomVelocity, up, velocity ); + velocity[2] += jump; + VectorAdd( velocity, playerVelocityScaled, velocity ); + VectorCopy( bodyAngles, angles ); + angles[PITCH] += 15; + CG_LaunchGib( origin, angles, velocity, cgs.media.gibLeg, Q_rand(&seed) ); + if (--numGibs <= 0) { + return; + } + } while (numGibs > 0); +} +void CG_GibPlayerOld( const vec3_t playerOrigin ) { + vec3_t origin, angles, velocity; if ( !cg_blood.integer ) { return; } + VectorClear(angles); + VectorCopy( playerOrigin, origin ); velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; if ( rand() & 1 ) { - CG_LaunchGib( origin, velocity, cgs.media.gibSkull ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibSkull, 0 ); } else { - CG_LaunchGib( origin, velocity, cgs.media.gibBrain ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibBrain, 0 ); } // allow gibs to be turned off for speed @@ -594,55 +886,55 @@ void CG_GibPlayer( const vec3_t playerOrigin ) { velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; - CG_LaunchGib( origin, velocity, cgs.media.gibAbdomen ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibAbdomen, 0 ); VectorCopy( playerOrigin, origin ); velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; - CG_LaunchGib( origin, velocity, cgs.media.gibArm ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibArm, 0 ); VectorCopy( playerOrigin, origin ); velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; - CG_LaunchGib( origin, velocity, cgs.media.gibChest ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibChest, 0 ); VectorCopy( playerOrigin, origin ); velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; - CG_LaunchGib( origin, velocity, cgs.media.gibFist ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibFist, 0 ); VectorCopy( playerOrigin, origin ); velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; - CG_LaunchGib( origin, velocity, cgs.media.gibFoot ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibFoot, 0 ); VectorCopy( playerOrigin, origin ); velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; - CG_LaunchGib( origin, velocity, cgs.media.gibForearm ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibForearm, 0 ); VectorCopy( playerOrigin, origin ); velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; - CG_LaunchGib( origin, velocity, cgs.media.gibIntestine ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibIntestine, 0 ); VectorCopy( playerOrigin, origin ); velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; - CG_LaunchGib( origin, velocity, cgs.media.gibLeg ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibLeg, 0 ); VectorCopy( playerOrigin, origin ); velocity[0] = crandom()*GIB_VELOCITY; velocity[1] = crandom()*GIB_VELOCITY; velocity[2] = GIB_JUMP + crandom()*GIB_VELOCITY; - CG_LaunchGib( origin, velocity, cgs.media.gibLeg ); + CG_LaunchGib( origin, angles, velocity, cgs.media.gibLeg, 0 ); } /* diff --git a/code/cgame/cg_event.c b/code/cgame/cg_event.c index 67319e7f..5a83f6a6 100644 --- a/code/cgame/cg_event.c +++ b/code/cgame/cg_event.c @@ -1215,7 +1215,64 @@ void CG_EntityEvent( centity_t *cent, vec3_t position, int entityNum ) { #else trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.gibSound ); #endif - CG_GibPlayer( cent->lerpOrigin ); + if (cg_oldGibs.integer) { + CG_GibPlayerOld( cent->lerpOrigin ); + } else { + int knockbackSpeed = cgs.g_gibsNewEvGibPlayerParmProtocol == 1 + ? es->eventParm * COMBAT_EV_GIB_PLAYER_ARG_DIVISOR + // Just use the default knockback speed for 100 damage. + : 100 * 1000 / COMBAT_PLAYER_MASS; + + lerpFrame_t torsoAnimation = es->number == cg.snap->ps.clientNum + // `cent->pe.torso` appears to be not good for self. + ? cg.predictedPlayerEntity.pe.torso + : cent->pe.torso; + vec3_t torsoAngles; + + // TODO fix: things like `origin` and `angles` + // are not in complete sync between clients, + // so this seed is not always the same for all players. + int randSeed = es->number; + randSeed = Q_rand(&randSeed) + es->clientNum; + randSeed = Q_rand(&randSeed) + es->eventParm; + randSeed = Q_rand(&randSeed) + cgs.levelStartTime; + // TODO fix: this varies from client to client. + // So for now we round it to make it in sync ~95% of the time. + randSeed = Q_rand(&randSeed) + cg.snap->serverTime / 2048; + if ( ci ) { + randSeed = Q_rand(&randSeed) + ci->name[0]; + } + + // Torso animation angles seem to be in better sync + // between the local state and how others see us, + // and overall are closer to other player's viewangles + // than `cent->lerpAngles`. + // `cent->lerpAngles`, seems to sometimes be pointing + // in a completely different direction than the player's body + // at the time of death. + // Moreover, for non-self pitch seems to be always + // not very far from 0. + // This could be related to `LookAtKiller()`. + // Also see `CG_PlayerAngles`. + torsoAngles[PITCH] = torsoAnimation.pitchAngle; + torsoAngles[YAW] = torsoAnimation.yawAngle; + torsoAngles[ROLL] = 0; + + if ( es->number == cg.snap->ps.clientNum ) { + // Apparently at this point `es->pos.trDelta` doesn't yet have + // the knockback from the damage that gibbed us, + // so we have to differentiate between self and non-self, + // and use `cg.predictedPlayerState.velocity` + // if it's ourself. + CG_GibPlayer( cent->lerpOrigin, torsoAngles, + cg.predictedPlayerState.velocity, knockbackSpeed, + &torsoAnimation, randSeed ); + } else { + CG_GibPlayer( cent->lerpOrigin, torsoAngles, + es->pos.trDelta, knockbackSpeed, + &torsoAnimation, randSeed ); + } + } break; case EV_STOPLOOPINGSOUND: diff --git a/code/cgame/cg_local.h b/code/cgame/cg_local.h index 240f6ed1..ed86d554 100644 --- a/code/cgame/cg_local.h +++ b/code/cgame/cg_local.h @@ -42,7 +42,7 @@ #define MAX_STEP_CHANGE 32 #define MAX_VERTS_ON_POLY 10 -#define MAX_MARK_POLYS 256 +#define MAX_MARK_POLYS 1024 #define STAT_MINUS 10 // num frame for '-' stats digit @@ -1136,6 +1136,7 @@ typedef struct { int pmove_msec; qboolean synchronousClients; + int g_gibsNewEvGibPlayerParmProtocol; int ospEnc; qboolean defrag; @@ -1406,7 +1407,10 @@ void CG_LightningBoltBeam( vec3_t start, vec3_t end ); #endif void CG_ScorePlum( int client, const vec3_t origin, int score ); -void CG_GibPlayer( const vec3_t playerOrigin ); +void CG_GibPlayer( const vec3_t playerOrigin, const vec3_t playerAngles, + const vec3_t playerVelocity, const int knockbackSpeed, + const lerpFrame_t *bodyAnimation, const int randSeed ); +void CG_GibPlayerOld( const vec3_t playerOrigin ); void CG_BigExplode( vec3_t playerOrigin ); void CG_Bleed( const vec3_t origin, int entityNum ); diff --git a/code/cgame/cg_localents.c b/code/cgame/cg_localents.c index 45535ab7..5e75da43 100644 --- a/code/cgame/cg_localents.c +++ b/code/cgame/cg_localents.c @@ -149,9 +149,11 @@ void CG_FragmentBounceMark( localEntity_t *le, trace_t *trace ) { } - // don't allow a fragment to make multiple marks, or they - // pile up while settling - le->leMarkType = LEMT_NONE; + // This is no longer needed, because we now decide whether to leave a mark + // purely based on impact velocity. + // // don't allow a fragment to make multiple marks, or they + // // pile up while settling + // le->leMarkType = LEMT_NONE; } /* @@ -179,18 +181,22 @@ void CG_FragmentBounceSound( localEntity_t *le, trace_t *trace ) { } - // don't allow a fragment to make multiple bounce sounds, - // or it gets too noisy as they settle - le->leBounceSoundType = LEBS_NONE; + // This is no longer needed, because we now decide whether to play a sound + // purely based on impact velocity. + // // don't allow a fragment to make multiple bounce sounds, + // // or it gets too noisy as they settle + // le->leBounceSoundType = LEBS_NONE; } /* ================ CG_ReflectVelocity + +Modifies velocity of `le` and writes the difference to `velocityDifference` ================ */ -void CG_ReflectVelocity( localEntity_t *le, trace_t *trace ) { +void CG_ReflectVelocity( localEntity_t *le, trace_t *trace, vec3_t velocityDifference ) { vec3_t velocity; float dot; int hitTime; @@ -203,6 +209,10 @@ void CG_ReflectVelocity( localEntity_t *le, trace_t *trace ) { VectorScale( le->pos.trDelta, le->bounceFactor, le->pos.trDelta ); + if (velocityDifference) { + VectorSubtract( le->pos.trDelta, velocity, velocityDifference ); + } + VectorCopy( trace->endpos, le->pos.trBase ); le->pos.trTime = cg.time; @@ -223,7 +233,7 @@ CG_AddFragment ================ */ static void CG_AddFragment( localEntity_t *le ) { - vec3_t newOrigin; + vec3_t newOrigin, impactVelocityDiff; trace_t trace; if ( le->pos.trType == TR_STATIONARY ) { @@ -283,14 +293,18 @@ static void CG_AddFragment( localEntity_t *le ) { return; } - // leave a mark - CG_FragmentBounceMark( le, &trace ); + // reflect the velocity on the trace plane + CG_ReflectVelocity( le, &trace, impactVelocityDiff ); - // do a bouncy sound - CG_FragmentBounceSound( le, &trace ); + if ( VectorLengthSquared( impactVelocityDiff ) >= Square( cg_bounceMarksMinImpactSpeed.value ) ) { + // leave a mark + CG_FragmentBounceMark( le, &trace ); + } - // reflect the velocity on the trace plane - CG_ReflectVelocity( le, &trace ); + if ( VectorLengthSquared( impactVelocityDiff ) >= Square( cg_bounceSoundMinImpactSpeed.value ) ) { + // do a bouncy sound + CG_FragmentBounceSound( le, &trace ); + } trap_R_AddRefEntityToScene( &le->refEntity ); } @@ -761,7 +775,26 @@ void CG_AddInvulnerabilityJuiced( localEntity_t *le ) { } if ( t > 5000 ) { le->endTime = 0; - CG_GibPlayer( le->refEntity.origin ); + if (cg_oldGibs.integer) { + CG_GibPlayerOld( le->refEntity.origin ); + } else { + vec3_t angles; + // Just use the default knockback speed for 200 damage. + int knockbackSpeed = 200 * 1000 / COMBAT_PLAYER_MASS; + int randSeed; + // Angles don't matter much here. + VectorClear( angles ); + // Since the player with invulnerability is not moving, + // we expect its position to be the same for all players, + // so we can use it as a seed. + randSeed = le->refEntity.origin[0] * 1024; + randSeed = Q_rand(&randSeed) + le->refEntity.origin[1] * 1024; + randSeed = Q_rand(&randSeed) + le->refEntity.origin[2] * 1024; + randSeed = Q_rand(&randSeed) + cgs.levelStartTime; + + CG_GibPlayer( le->refEntity.origin, angles, le->pos.trDelta, + knockbackSpeed, NULL, randSeed ); + } } else { trap_R_AddRefEntityToScene( &le->refEntity ); diff --git a/code/cgame/cg_servercmds.c b/code/cgame/cg_servercmds.c index d47cfd9b..a40e3a5a 100644 --- a/code/cgame/cg_servercmds.c +++ b/code/cgame/cg_servercmds.c @@ -159,6 +159,8 @@ void CG_ParseSysteminfo( void ) { } cgs.synchronousClients = ( atoi( Info_ValueForKey( info, "g_synchronousClients" ) ) ) ? qtrue : qfalse; + cgs.g_gibsNewEvGibPlayerParmProtocol = + atoi( Info_ValueForKey( info, "g_gibsNewEvGibPlayerParmProtocol" ) ); } diff --git a/code/game/bg_public.h b/code/game/bg_public.h index 0878102e..95ec9c8b 100644 --- a/code/game/bg_public.h +++ b/code/game/bg_public.h @@ -185,6 +185,13 @@ void Pmove (pmove_t *pmove); //=================================================================================== +#define COMBAT_PLAYER_MASS 200 +// A divisor of knockback speed, to fit it into one byte. +// By dividing by 8 we can represent a speed of up to (255 * 8) = 2040. +// For comparison, with `g_knockback` of 1000 and `MAX_KNOCKBACK` of 200 +// the max knockback speed in most situations is 1000. +#define COMBAT_EV_GIB_PLAYER_ARG_DIVISOR 8 + // player_state->stats[] indexes // NOTE: may not have more than 16 diff --git a/code/game/g_active.c b/code/game/g_active.c index ad5d2878..f92b8db8 100644 --- a/code/game/g_active.c +++ b/code/game/g_active.c @@ -58,13 +58,6 @@ void P_DamageFeedback( gentity_t *player ) { client->ps.damageCount = count; - - // - // clear totals - // - client->damage_blood = 0; - client->damage_armor = 0; - client->damage_knockback = 0; } @@ -1154,6 +1147,12 @@ void ClientEndFrame( gentity_t *ent ) { client->ps.stats[STAT_HEALTH] = ent->health; // FIXME: get rid of ent->health... + // This is not present in the original game code, + // see comments about `FL_NO_KNOCKBACK` in `g_combat`. + if ( client->ps.pm_type & PM_DEAD ) { + ent->flags |= FL_NO_KNOCKBACK; + } + G_SetClientSound( ent ); // set the latest info @@ -1217,6 +1216,13 @@ void ClientEndFrame( gentity_t *ent ) { client->damage.team = 0; } + // + // clear damage totals + // + client->damage_blood = 0; + client->damage_armor = 0; + client->damage_knockback = 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); diff --git a/code/game/g_combat.c b/code/game/g_combat.c index 5a88e713..e2909d35 100644 --- a/code/game/g_combat.c +++ b/code/game/g_combat.c @@ -215,12 +215,24 @@ void LookAtKiller( gentity_t *self, gentity_t *inflictor, gentity_t *attacker ) self->client->ps.stats[STAT_DEAD_YAW] = vectoyaw ( dir ); } +#define MAX_KNOCKBACK 200 +/* +================== +KnockbackToKnockbackSpeed +================== +*/ +static float KnockbackToKnockbackSpeed( int knockback ) { + return g_knockback.value * (float)knockback / COMBAT_PLAYER_MASS; +} + + /* ================== GibEntity ================== */ -void GibEntity( gentity_t *self, int killer ) { +void GibEntity( gentity_t *self, int killer, int damageBloodFallback ) { + int eventParm = killer; #ifdef MISSIONPACK gentity_t *ent; int i; @@ -242,7 +254,51 @@ void GibEntity( gentity_t *self, int killer ) { } #endif - G_AddEvent( self, EV_GIB_PLAYER, killer ); + + // In vanilla Quake the meaning of the `EV_GIB_PLAYER` eventParm + // is `killer`. + // But it is unused client-side, so it's safe for us to change its meaning + // (i.e. to change the network protocol). + // However, some mods might in fact rely on it, + // so let's have a CVAR to keep the old behavior. + // + // Note that we're not checking `g_oldGibs`, because in itself + // this does not affect behavior: + // we're simply providing the client with the knockback info, + // and whether to use that into is up to `cg_oldGibs`. + if ( g_gibsNewEvGibPlayerParmProtocol.integer == 1 ) { + int damage; + float knockbackSpeed; + + // We prefer actual damage over `client->damage_knockback` + // because `damage_knockback` is sometimes undesirably 0. Namely: + // - when the target is a dead body, with `FL_NO_KNOCKBACK`. + // - when the knockback `dir` is not provided to `G_Damage`, + // such as with crushers. + // + // Most of the time (but not always e.g. with lava) + // "no knockback" means "the player should not be moved + // in any particular direction", + // and not that "their gibs should stay put". + damage = self->client + ? self->client->damage_blood + self->client->damage_armor + : damageBloodFallback; + if ( damage > MAX_KNOCKBACK ) { + damage = MAX_KNOCKBACK; + } + + knockbackSpeed = KnockbackToKnockbackSpeed( damage ); + + // Fit it into one byte. + eventParm = knockbackSpeed / COMBAT_EV_GIB_PLAYER_ARG_DIVISOR; + if (eventParm > 255) { + eventParm = 255; + } + } else { + eventParm = killer; + } + G_AddEvent( self, EV_GIB_PLAYER, eventParm ); + self->takedamage = qfalse; self->s.eType = ET_INVISIBLE; self->r.contents = 0; @@ -262,7 +318,7 @@ void body_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int d return; } - GibEntity( self, 0 ); + GibEntity( self, 0, damage ); } @@ -589,11 +645,10 @@ void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int // unless you aim at the feet. // See https://github.com/ioquake/ioq3/issues/794. // - // Note that without this line, when shooting at two players standing - // behind each other, the second target will take less damage, - // because the dead body of the first player will absorb the pellets - // until it gets gibbed (that is, up to 4 pellets, - // see `GIB_HEALTH` and `DEFAULT_SHOTGUN_DAMAGE`). + // Not executing this line makes is so that the corpse + // doesn't get shorter immediately on death + // and instead can still take up other pellets + // from the same shotgun shot. // // The purpose and the effect of this line is not entirely clear. // Maybe it's to transition the player hitbox @@ -613,7 +668,7 @@ void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int // never gib in a nodrop if ( (self->health <= GIB_HEALTH && !(contents & CONTENTS_NODROP) && g_blood.integer) || meansOfDeath == MOD_SUICIDE) { // gib death - GibEntity( self, killer ); + GibEntity( self, killer, damage ); } else { // normal death static int i; @@ -774,6 +829,77 @@ int G_InvulnerabilityEffect( gentity_t *targ, vec3_t dir, vec3_t point, vec3_t i } } #endif + +/* +================ +AdjustKnockbackIfDirectMissileHit + +Adjusts knockback direction from missiles' direct hits so gibs look better. +By default the knockback direction is the direction +in which the missile is flying (see `G_MissileImpact`), +which is not great when the missile hits just the edge of the player's feet. +One would expect that the gibs fly up then. +Which is what this function ensures. + +Assumes that the new `targ->health` is already set. +================ +*/ +static void AdjustKnockbackIfDirectMissileHit( const gentity_t *targ, + const gentity_t *inflictor, const vec3_t dir, const vec3_t point, + int knockback, const vec3_t oldKvel, int dflags, int mod, vec3_t velChange ) +{ + vec3_t dir2; // Direction from the explosion to the player's center. + vec3_t kvel2, finalDir; + + VectorClear( velChange ); + + if (!( + knockback && targ->client && + inflictor && + inflictor->s.eType == ET_MISSILE && + // Make sure it has big splash radius, + // which e.g. is not the case for the nailgun + // (only damages on direct hit) and plasmagun (small explosion radius). + inflictor->splashRadius > 40 && inflictor->splashDamage > 0 && + // But we only handle direct hits here. + !( dflags & DAMAGE_RADIUS ) + // Another way to check for direct hits. + // mod != inflictor->splashMethodOfDeath + )) { + return; + } + + // Note that the missile direction and the direction + // from the explosion to the origin could be quite different, + // so we need to calculate the direction first + // instead of applying velocities right away, + // which would have resulted in less knockback speed. + + // Copy-pasted from `G_RadiusDamage`. + VectorSubtract (targ->r.currentOrigin, point, dir2); + // Set a value lower than the original 24 + // because that more closely corresponds to the position of the chest. + // dir2[2] += 24; + dir2[2] += 20; + if ( VectorNormalize( dir2 ) <= 0.0 ) { + return; + } + + VectorClear( finalDir ); + VectorMA( finalDir, g_gibsMissileDirectionKnockbackWeight.value, dir, finalDir ); + VectorMA( finalDir, (1 - g_gibsMissileDirectionKnockbackWeight.value), dir2, finalDir ); + if ( VectorNormalize( finalDir ) <= 0.0 ) { + // No particular direction, so let's just apply no knockback at all. + VectorScale( oldKvel, -1, velChange ); + return; + } + + // "Cancel" the old knockback. + VectorScale( oldKvel, -1, velChange ); + VectorScale (finalDir, KnockbackToKnockbackSpeed( knockback ), kvel2); + VectorAdd (velChange, kvel2, velChange); +} + /* ============ G_Damage @@ -804,11 +930,14 @@ void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, int take; int asave; int knockback; + vec3_t kvel; int max; #ifdef MISSIONPACK vec3_t bouncedir, impactpoint; #endif + VectorClear( kvel ); + if (!targ->takedamage) { return; } @@ -897,8 +1026,8 @@ void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, } knockback = damage; - if ( knockback > 200 ) { - knockback = 200; + if ( knockback > MAX_KNOCKBACK ) { + knockback = MAX_KNOCKBACK; } if ( targ->flags & FL_NO_KNOCKBACK ) { knockback = 0; @@ -909,12 +1038,7 @@ void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, // 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); + VectorScale (dir, KnockbackToKnockbackSpeed( knockback ), kvel); VectorAdd (targ->client->ps.velocity, kvel, targ->client->ps.velocity); // set the timer so that the other client can't cancel @@ -1064,12 +1188,50 @@ void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, } if ( targ->health <= 0 ) { - if ( client ) - targ->flags |= FL_NO_KNOCKBACK; + // In the original code we used to set `FL_NO_KNOCKBACK` here. + // However, that made it so that when fragging with the shotgun, + // the dead body does not gain momentum (knockback) + // from the pellets that come after the pellet + // that made the health go below 0. + // That resulted in the dead body not getting pushed + // as far as it should have been, and, most importantly, + // the gibs not getting enough momentum. See + // https://github.com/ec-/baseq3a/pull/53. + // + // Now we set the `FL_NO_KNOCKBACK` flag inside of `ClientEndFrame`, + // which is ran after all the pellets of the shotgun shot + // have done their thing, + // i.e. `FL_NO_KNOCKBACK` takes effect only on the next frame. + // + // Note that if the body gets gibbed then + // it will still stop absorbing pellets, + // i.e. this fix only adds at most `GIB_HEALTH` worth of knockback. + // + // This issiue is similar to + // https://github.com/ioquake/ioq3/issues/794. + // + // if ( client ) + // targ->flags |= FL_NO_KNOCKBACK; if (targ->health < -999) targ->health = -999; + if ( + // If it's not a gib death, do not apply this adjustment, + // because some might say that it would affect gameplay. + // Namely that dead bodies can e.g. absorb missiles, + // so it _does_ matter where they fly. + // The condition is copy-pasted from `player_die` (partially). + targ->health <= GIB_HEALTH && g_blood.integer && + !g_oldGibs.integer && + g_gibsMissileDirectionKnockbackWeight.value != 1.0 && + targ->client ) { + vec3_t velChange; + AdjustKnockbackIfDirectMissileHit( targ, inflictor, dir, point, + knockback, kvel, dflags, mod, velChange ); + VectorAdd(targ->client->ps.velocity, velChange, targ->client->ps.velocity); + } + targ->enemy = attacker; targ->die (targ, inflictor, attacker, take, mod); return; diff --git a/code/game/g_cvar.h b/code/game/g_cvar.h index c9a8ad9b..15b6d73e 100644 --- a/code/game/g_cvar.h +++ b/code/game/g_cvar.h @@ -62,6 +62,20 @@ G_CVAR( g_debugDamage, "g_debugDamage", "0", 0, 0, qfalse, qfalse ) G_CVAR( g_debugAlloc, "g_debugAlloc", "0", 0, 0, qfalse, qfalse ) G_CVAR( g_motd, "g_motd", "", 0, 0, qfalse, qfalse ) G_CVAR( g_blood, "com_blood", "1", 0, 0, qfalse, qfalse ) +G_CVAR( g_oldGibs, "g_oldGibs", "0", CVAR_ARCHIVE, 0, qfalse, qfalse ) +// How much the movement direction of a missile affects the knockback direction +// when gibbing, as opposed to the direction from the center of the explosion. +// This makes sure that if the missile hits the player's feet, +// the gibs will fly up, so that there is not a big difference +// between hitting the player's feet and the ground not far +// from the player's feet. +// +// Setting to 1 restores the old behavior. +// +// Note that this affects not just the gibs +// but also the camera velocity of the gibbed player. +G_CVAR( g_gibsMissileDirectionKnockbackWeight, "g_gibsMissileDirectionKnockbackWeight", "0.5", CVAR_ARCHIVE, 0, qfalse, qfalse ) +G_CVAR( g_gibsNewEvGibPlayerParmProtocol, "g_gibsNewEvGibPlayerParmProtocol", "1", CVAR_SYSTEMINFO | CVAR_ARCHIVE, 0, qfalse, qfalse ) G_CVAR( g_podiumDist, "g_podiumDist", "80", 0, 0, qfalse, qfalse ) G_CVAR( g_podiumDrop, "g_podiumDrop", "70", 0, 0, qfalse, qfalse ) diff --git a/code/game/g_missile.c b/code/game/g_missile.c index 8f3418d2..b4dbd0d0 100644 --- a/code/game/g_missile.c +++ b/code/game/g_missile.c @@ -298,8 +298,11 @@ void G_MissileImpact( gentity_t *ent, trace_t *trace ) { if ( VectorLength( velocity ) == 0 ) { velocity[2] = 1; // stepped on a grenade } + // Originally we used `ent->s.origin` instead of `trace->endpos`, + // but the latter is more accurate. + // We also already use it below for `G_RadiusDamage`. G_Damage (other, ent, &g_entities[ent->r.ownerNum], velocity, - ent->s.origin, ent->damage, + trace->endpos, ent->damage, 0, ent->methodOfDeath); } } diff --git a/code/game/g_weapon.c b/code/game/g_weapon.c index 8bd49853..6b40cb3e 100644 --- a/code/game/g_weapon.c +++ b/code/game/g_weapon.c @@ -317,6 +317,27 @@ static qboolean ShotgunPellet( const vec3_t start, const vec3_t end, gentity_t * continue; } #else + + // The below piece of code has been added in + // https://github.com/ec-/baseq3a/pull/60. + // When shooting through a corpse, gib it, + // but don't "absorb" the pellet, i.e. allow to hit a player + // through a corpse. + // This is mostly to compensate for the balance changes + // that are introduced by the removal of the `self->r.maxs[2] = -8;` + // line in `player_die`. + // But it's probably also sensible otherwise that corpses + // affect "more serious" gameplay less. + // See + // - https://github.com/ioquake/ioq3/issues/794 + // - https://github.com/OpenArena/gamecode/pull/349 + if ( traceEnt->client && traceEnt->client->ps.pm_type == PM_DEAD ) { + G_Damage( traceEnt, ent, ent, forward, tr.endpos, damage, 0, MOD_SHOTGUN ); + passent = traceEnt->s.number; + VectorCopy( tr.endpos, tr_start ); + continue; + } + if ( LogAccuracyHit( traceEnt, ent ) ) { hitClient = qtrue; }