// Copyright (C) 2001-2002 Raven Software // // g_weapon.c // perform the server side effects of a weapon firing #include "g_local.h" #include "be_aas.h" extern qboolean G_BoxInBounds( vec3_t point, vec3_t mins, vec3_t maxs, vec3_t boundsMins, vec3_t boundsMaxs ); typedef struct hitLocationConversion_s { int SOF2poses; int hitLocation; float damageMultiplier; } hitLocationConversion_t; #define HITLOC_HEAD_DM -1.0f #define HITLOC_NECK_DM -2.0f #define HL_CONV_MAX 18 hitLocationConversion_t hitLocationConversion[HL_CONV_MAX] = { {-1, HL_NONE ,0 }, // UNKNOWN {0, HL_HEAD ,HITLOC_HEAD_DM }, // "HEAD" {1, HL_LEG_LOWER_RT ,0.7f}, // "RIGHT LEG" {4, HL_NECK ,HITLOC_NECK_DM}, // "NECK" {8, HL_CHEST ,1.0f}, // "CHEST" {12, HL_FOOT_LT ,0.4f}, // "RIGHT FOOT" {16, HL_CHEST_LT ,1.0f}, // "LEFT SHOULDER" {20, HL_ARM_LT ,0.7f}, // "LEFT ARM" {24, HL_HAND_LT ,0.4f}, // "LEFT HAND" {28, HL_CHEST_RT ,1.0f}, // "RIGHT SHOULDER" {32, HL_ARM_RT ,0.7f}, // "RIGHT ARM" {36, HL_HAND_RT ,0.4f}, // "RIGHT HAND" {40, HL_WAIST ,1.0f}, // "GUT" {44, HL_WAIST ,1.0f}, // "GROIN" {48, HL_LEG_UPPER_LT ,0.7f}, // "LEFT THIGH" {52, HL_LEG_LOWER_LT ,0.7f}, // "LEFT LEG" {56, HL_FOOT_RT ,0.4f}, // "LEFT FOOT" {60, HL_LEG_UPPER_RT ,0.7f}, // "RIGHT THIGH" }; /* =============== G_CheckSniperCall Checks to see if the client or someone near him should yell "sniper" =============== */ void G_CheckSniperCall ( weapon_t weapon, gentity_t* attacker, gentity_t* target, qboolean miss ) { vec3_t diff; // If it was a sniper that shot this guy then play the sniper voice if ( !G_CanVoiceGlobal ( ) ) { return; } // Needs to have been a snipe rifle and an enemy if ( weapon != WP_MSG90A1 || OnSameTeam ( attacker, target ) ) { return; } // Make sure hes not close VectorSubtract ( attacker->r.currentOrigin, target->r.currentOrigin, diff ); if ( VectorLengthSquared ( diff ) > 1500 * 1500 ) { gentity_t* nearby; // Find someone close nearby = G_FindNearbyClient ( target->r.currentOrigin, target->client->sess.team, 800, target ); // if it was a miss have the guy that was shot at say it if ( miss ) { if ( nearby ) { G_VoiceGlobal ( target, "sniper", qfalse ); } } // Look for someone around the guy being shot at to say sniper else if ( nearby ) { G_VoiceGlobal ( nearby, "sniper", qfalse ); } } } /* =============== G_TraceBullet Runs a trace for a fired bullet =============== */ void G_TraceBullet ( weapon_t weapon, trace_t* tr, G2Trace_t G2Trace, vec3_t start, vec3_t end, int passent, int mask, qboolean detailed ) { int unlinkCount; gentity_t* unlinked[20]; unlinkCount = 0; G2Trace[0].mEntityNum = -1; while ( 1 ) { // Run the trace as is trap_Trace ( tr, start, NULL, NULL, end, passent, mask ); // If the bullet hit glass then break it completely for now if ( tr->fraction != 1 && !Q_stricmp ( g_entities[ tr->entityNum ].classname, "func_glass" ) ) { // break the glass g_entities[ tr->entityNum ].use ( &g_entities[ tr->entityNum ], &g_entities[ tr->entityNum ], &g_entities[ tr->entityNum ] ); continue; } // If we hit a client, do a detailed trace if ( detailed && tr->fraction != 1 && g_entities[ tr->entityNum ].client ) { animation_t* anim; gentity_t* traceEnt; trace_t vtr; traceEnt = &g_entities[ tr->entityNum ]; anim = &level.ghoulAnimations[traceEnt->client->legs.anim&(~ANIM_TOGGLEBIT)]; trap_G2API_SetBoneAnim(level.serverGhoul2, 0, "model_root", anim->firstFrame, anim->firstFrame + anim->numFrames, BONE_ANIM_OVERRIDE_LOOP, 50.0f / anim->frameLerp, traceEnt->client->legs.animTime, -1, 0); anim = &level.ghoulAnimations[traceEnt->client->torso.anim&(~ANIM_TOGGLEBIT)]; trap_G2API_SetBoneAnim(level.serverGhoul2, 0, "lower_lumbar", anim->firstFrame, anim->firstFrame + anim->numFrames, BONE_ANIM_OVERRIDE_LOOP, 50.0f / anim->frameLerp, traceEnt->client->torso.animTime, -1, 0); trap_G2API_SetBoneAngles( level.serverGhoul2, 0, "upper_lumbar", traceEnt->client->ghoulUpperTorsoAngles, BONE_ANGLES_POSTMULT, POSITIVE_X, NEGATIVE_Y, NEGATIVE_Z, 0, 0, level.time ); trap_G2API_SetBoneAngles( level.serverGhoul2, 0, "lower_lumbar", traceEnt->client->ghoulLowerTorsoAngles, BONE_ANGLES_POSTMULT, POSITIVE_X, NEGATIVE_Y, NEGATIVE_Z, 0, 0, level.time ); trap_G2API_SetBoneAngles( level.serverGhoul2, 0, "cranium", traceEnt->client->ghoulHeadAngles, BONE_ANGLES_POSTMULT, POSITIVE_Z, NEGATIVE_Y, POSITIVE_X, 0,0, level.time ); trap_G2API_CollisionDetect ( G2Trace, level.serverGhoul2, traceEnt->client->ghoulLegsAngles, traceEnt->r.currentOrigin, level.time, traceEnt->s.number, start, end, vec3_identity, 0, 2 ); // Check to see if anyone should yell SNIPER! G_CheckSniperCall ( weapon, &g_entities[passent], traceEnt, G2Trace[0].mEntityNum != traceEnt->s.number ? qtrue : qfalse ); // Did we hit? if ( G2Trace[0].mEntityNum == -1 ) { unlinked[unlinkCount++] = traceEnt; trap_UnlinkEntity ( traceEnt ); continue; } trap_UnlinkEntity ( traceEnt ); // Unfortunately because the players bounding box is doubled we have to run // on more trace to make sure the player is actually able to be hit where they // were hit. The bounding box is doubled to ensure that the entire ghoul model // is accounted for, but that means the player will stick through walls too. To check // for this we run a trace from the hit location on the big bounding box to the // hit location of the ghoul model and make sure there is nothing solid in the way trap_Trace ( &vtr, tr->endpos, NULL, NULL, G2Trace[0].mCollisionPosition, passent, MASK_SHOT&(~CONTENTS_BODY) ); if ( vtr.entityNum != G2Trace[0].mEntityNum ) { if ( vtr.entityNum != ENTITYNUM_NONE ) { // Must have hit something, discount the trace unlinked[unlinkCount++] = traceEnt; continue; } } trap_LinkEntity ( traceEnt ); } break; } // Relink all the unlinked entities while ( unlinkCount > 0 ) { trap_LinkEntity ( unlinked[--unlinkCount] ); } } #define MAX_HITS 20 typedef struct gbullethit_s { gentity_t* ent; vec3_t origin; int damage; int location; } gbullethit_t; /* =============== G_FireBullet Fires a bullet from the given player entity using the given weapon and attack =============== */ void G_FireBullet ( gentity_t* ent, int weapon, int attack ) { trace_t tr; vec3_t end; gentity_t *tent; gentity_t *traceEnt; int i; vec3_t muzzlePoint; vec3_t fwd; vec3_t right; vec3_t up; vec3_t fireAngs; float damageMult; int hitcount; G2Trace_t G2Trace; int maxFx; weaponData_t* weaponDat; attackData_t* attackDat; float inaccuracy; qboolean detailed; int seed; gbullethit_t hit[MAX_HITS]; // Grab the firing info weaponDat = &weaponData[ent->s.weapon]; attackDat = &weaponDat->attack[attack]; // Maximum effects that can be spawned from this bullet maxFx = (attackDat->pellets / 2) + 1; hitcount = 0; // Detailed trace or not? detailed = (attackDat->pellets > 1 || attackDat->melee) ? qfalse : qtrue; // Current inaccuracy inaccuracy = (float)ent->client->ps.inaccuracy / 1000.0f; if ( detailed ) { if ( ent->client->ps.pm_flags & PMF_JUMPING ) { inaccuracy *= JUMP_ACCURACY_MODIFIER; } else if ( ent->client->ps.pm_flags & PMF_DUCKED ) { inaccuracy *= DUCK_ACCURACY_MODIFIER; } } // Anti-lag G_ApplyAntiLag ( ent, detailed ); // where is gun muzzle? VectorCopy( ent->client->ps.origin, muzzlePoint ); muzzlePoint[2] += ent->client->ps.viewheight; // Handle leaning VectorCopy(ent->client->ps.viewangles, fireAngs); AngleVectors( fireAngs, fwd, right, up); if ( ent->client->ps.pm_flags & PMF_LEANING ) { BG_ApplyLeanOffset ( &ent->client->ps, muzzlePoint ); } // Move the start trace back a bit to account for bumping up against someone VectorMA ( muzzlePoint, -15, fwd, muzzlePoint ); seed = ent->client->ps.stats[STAT_SEED]; // Run a trace for each pellet being fired for (i = 0; i < attackDat->pellets; i++) { int location = HL_NONE; // Determine the endpoint for the bullet BG_CalculateBulletEndpoint ( muzzlePoint, fireAngs, inaccuracy, attackDat->rV.range + 15, end, &seed ); // Trace the bullet G_TraceBullet ( weapon, &tr, G2Trace, muzzlePoint, end, ent->s.number, MASK_SHOT, detailed ); if ( (tr.surfaceFlags & SURF_NOIMPACT) || tr.entityNum == ENTITYNUM_NONE ) { // a big miss continue; } traceEnt = &g_entities[ tr.entityNum ]; // snap the endpos to integers, but nudged towards the line SnapVectorTowards( tr.endpos, muzzlePoint ); damageMult = 1.0f; if ( traceEnt->takedamage ) { // Where did the bullet hit? if ( ent->client && traceEnt->client ) { // Add or subtract some damage depending on where it hits if ( G2Trace[0].mEntityNum == -1 ) { vec3_t dir; VectorSubtract ( end, muzzlePoint, dir ); VectorNormalize ( dir ); location = G_GetHitLocation ( traceEnt, muzzlePoint, dir ); switch ( location ) { case HL_FOOT_RT: case HL_FOOT_LT: case HL_HAND_RT: case HL_HAND_LT: damageMult = 0.3f; break; case HL_ARM_RT: case HL_ARM_LT: case HL_LEG_UPPER_RT: case HL_LEG_UPPER_LT: case HL_LEG_LOWER_RT: case HL_LEG_LOWER_LT: damageMult = 0.7f; break; case HL_HEAD: case HL_NECK: damageMult = 1.75f; break; default: case HL_BACK_RT: case HL_BACK_LT: case HL_BACK: case HL_CHEST_RT: case HL_CHEST_LT: case HL_CHEST: case HL_WAIST: damageMult = 1.0f; break; case HL_NONE: damageMult = 0.0f; break; } } else { int z; float accuracyRatio; float maxinaccuracy; float addinaccuracy; location = HL_NONE; damageMult = 0; accuracyRatio = 1.0f; maxinaccuracy = attackDat->maxInaccuracy / 1000.0f; addinaccuracy = attackDat->inaccuracy / 1000.0f; if ( maxinaccuracy ) { // Calculate the ratio of how far along the inaccuracy spread they are accuracyRatio = 1.0f - ((inaccuracy - addinaccuracy) / (maxinaccuracy)); accuracyRatio = Com_Clampf ( 0.0f, 1.0f, accuracyRatio ); } for ( z = 0; z < MAX_G2_COLLISIONS && G2Trace[z].mEntityNum != -1; z ++ ) { int temp_location; float temp_damageMult; int pose; int l; pose = ( G2Trace[z].mLocation >> 2 ); temp_damageMult = 0.0f; temp_location = HL_NONE; // Convert hitregions (SOF2.poses) to hitlocation for ( l=0; l < HL_CONV_MAX; l++) { // Found the SOF2pose if (hitLocationConversion[l].SOF2poses == pose ) { temp_location = hitLocationConversion[l].hitLocation; // Special cases if (hitLocationConversion[l].damageMultiplier==HITLOC_HEAD_DM) // Head { temp_damageMult = 1.0f + 2.0f * accuracyRatio; } else if (hitLocationConversion[l].damageMultiplier==HITLOC_NECK_DM) // Neck { temp_damageMult = 1.0f + 0.75f * accuracyRatio; } else { temp_damageMult = hitLocationConversion[l].damageMultiplier; } break; } } // Didn't find it? Default to waist if (temp_location == HL_NONE) { temp_location = HL_WAIST; // Search to find waist damageMultiplier for ( l=0; l < HL_CONV_MAX; l++) { if (temp_location == hitLocationConversion[l].hitLocation) { temp_damageMult = hitLocationConversion[l].damageMultiplier; break; } } } if ( temp_damageMult > damageMult ) { location = temp_location; damageMult = temp_damageMult; } } } } // If a range attack then do distance affected damage, but exclude close range attacks if ( attackDat->rV.range > 100 ) { vec3_t diff; float dist; float checkrange; VectorSubtract ( muzzlePoint, tr.endpos, diff ); dist = VectorLength ( diff ); checkrange = attackDat->rV.range * 0.5f; // If half the way through its range then start falling off the damage to 1 quarter damage if ( dist > attackDat->rV.range * 0.5f ) { dist -= (attackDat->rV.range * 0.5f); // Scale oof the damage damageMult *= (1.0f - (dist / (float)checkrange)); } } // we hit something that noticed, so that is enough pellets if ( (int)(attackDat->damage * damageMult) > 0 ) { if ( !attackDat->melee ) { damageMult *= 1.15f; } hit[hitcount].ent = traceEnt; hit[hitcount].damage = attackDat->damage * damageMult; hit[hitcount].location = location; VectorCopy ( tr.endpos, hit[hitcount].origin ); hitcount++; } } // send bullet impact if (i < maxFx && attackDat->damage * damageMult > 0.0f ) { qboolean flesh = qfalse; // Are they are a client? if ( traceEnt->client ) { tr.surfaceFlags = 0; // Invulnerable? if ( level.time - traceEnt->client->invulnerableTime >= g_respawnInvulnerability.integer * 1000 ) { // Shot my a teammate with ff off? if ( !level.gametypeData->teams || (ent->client && !(OnSameTeam(ent,traceEnt) && (!g_friendlyFire.integer || level.warmupTime) ) ) ) { flesh = qtrue; } } } // If it was a client that was hit and it wasnt by a teammate when friendly fire is off then send blood! if ( flesh ) { tent = G_TempEntity( tr.endpos, EV_BULLET_HIT_FLESH ); // send entity and direction tent->s.eventParm = DirToByte( fwd ); tent->s.otherEntityNum = ent->s.number; tent->s.otherEntityNum2 = traceEnt->s.number; // Pack the shot info into the temp end for gore tent->s.time = weapon + ((attack&0xFF)<<8) + ((((int)traceEnt->s.apos.trBase[YAW]&0x7FFF) % 360) << 16); VectorCopy ( traceEnt->r.currentOrigin, tent->s.angles ); SnapVector ( tent->s.angles ); // Some procedural gore should be ignored because it would look odd on the player. For // example, if someone gets shot in the head but doesnt die from it. if ( traceEnt->client->ps.stats[STAT_HEALTH] > 0 ) { if ( location & (HL_HEAD) ) { tent->s.time |= GORE_NONE; } } } else { tent = G_TempEntity( tr.endpos, EV_BULLET_HIT_WALL ); // send direction and material tent->s.eventParm = DirToByte( tr.plane.normal ); tent->s.eventParm <<= MATERIAL_BITS; tent->s.eventParm |= (tr.surfaceFlags & MATERIAL_MASK); tent->s.time = weapon + ((attack&0xFF)<<8); tent->r.detailTime = level.time + rand() % 1000; tent->r.svFlags |= SVF_DETAIL; } tent->s.otherEntityNum = ent->s.number; } } // Push all the clients back to the real timeframe. We need to do this // before damaging the client so the real bounding box and location are stored in // the body G_UndoAntiLag ( ); if ( hitcount ) { int flags; int h; flags = 0; flags |= (attackDat->gore ? 0 : DAMAGE_NO_GORE); flags |= (attackDat->melee ? DAMAGE_NO_ARMOR : 0); flags |= (hitcount > 2 ? DAMAGE_FORCE_GORE : 0 ); for ( h = 0; h < hitcount; h ++ ) { G_Damage( hit[h].ent, ent, ent, fwd, hit[h].origin, hit[h].damage, flags, attackDat->mod + (attack<<8), hit[h].location ); } } } /* =============== G_FireProjectile Fires a projectile from the given client entity =============== */ gentity_t* G_FireProjectile ( gentity_t *ent, weapon_t weapon, attackType_t attack, int projectileLifetime, int flags ) { int i; vec3_t muzzlePoint; vec3_t fwd, right, up, fireAngs; gentity_t* missile = NULL; weaponData_t* weaponDat; attackData_t* attackDat; float inaccuracy; // Grab the firing info weaponDat = &weaponData[ent->s.weapon]; attackDat = &weaponDat->attack[attack]; inaccuracy = attackDat->inaccuracy / 1000.0f; // where is gun muzzle? VectorCopy( ent->client->ps.origin, muzzlePoint ); muzzlePoint[2] += ent->client->ps.viewheight; // Inform of the grenade toss if its a timed grenade if ( weapon >= WP_M84_GRENADE && weapon < WP_M15_GRENADE && (flags&PROJECTILE_TIMED) && (ent->client->ps.pm_type == PM_NORMAL) ) { gentity_t* nearby; // Make sure there is someone nearby to hear you yell nearby = G_FindNearbyClient ( ent->r.currentOrigin, ent->client->sess.team, 1200, ent ); if ( nearby ) { G_VoiceGlobal ( ent, "fire_hole", qtrue ); } } // snap to integer coordinates for more efficient network bandwidth usage SnapVector( muzzlePoint ); VectorCopy(ent->client->ps.viewangles, fireAngs); if ( ent->client->ps.pm_flags & PMF_LEANING ) { BG_ApplyLeanOffset ( &ent->client->ps, muzzlePoint ); } AngleVectors( fireAngs, fwd, right, up ); for (i = 0; i < attackDat->pellets; i++) { vec3_t dir; VectorCopy( fwd, dir ); if ( inaccuracy != 0) { // add in some spread / scatter dir[0] += flrand(-0.1 * inaccuracy, 0.1 * inaccuracy); dir[1] += flrand(-0.1 * inaccuracy, 0.1 * inaccuracy); dir[2] += flrand(-0.1 * inaccuracy, 0.1 * inaccuracy); } missile = G_CreateMissile( muzzlePoint, dir, attackDat->rV.velocity, min(projectileLifetime, 10000), ent, attack ); missile->classname = ammoData[attackDat->ammoIndex].name; missile->s.weapon = weapon; VectorSet( missile->r.maxs, 1, 1, 1 ); VectorScale( missile->r.maxs, -1, missile->r.mins ); missile->damage = attackDat->damage; missile->dflags = DAMAGE_DEATH_KNOCKBACK|DAMAGE_NO_ARMOR; missile->dflags |= (attackDat->gore ? 0 : DAMAGE_NO_GORE); missile->methodOfDeath = attackDat->mod + (attack<<8); missile->clipmask = MASK_SHOT | CONTENTS_MISSILECLIP; if(attackDat->splashRadius) { missile->splashDamage = attackDat->damage; missile->splashRadius = attackDat->splashRadius; missile->splashMethodOfDeath = missile->methodOfDeath; } else { missile->splashDamage = 0; missile->splashRadius = 0; missile->splashMethodOfDeath = MOD_UNKNOWN; //?? } if (flags & PROJECTILE_GRAVITY) { missile->s.pos.trType = TR_GRAVITY; } else if (flags & PROJECTILE_LIGHTGRAVITY) { missile->s.pos.trType = TR_LIGHTGRAVITY; } if (flags & PROJECTILE_TIMED) { missile->s.eFlags |= EF_BOUNCE_SCALE; missile->think = G_GrenadeThink; missile->bounceScale = attackDat->bounceScale; } else { // we don't want it to bounce, just blow up missile->bounceScale = 0; missile->think = G_ExplodeMissile; } if (flags & PROJECTILE_DAMAGE_AREA) { // add area damage over time missile->damage /= 2; missile->dflags |= DAMAGE_AREA_DAMAGE; } } // Angle override on the knife if ( weapon == WP_KNIFE ) { missile->s.eFlags |= EF_ANGLE_OVERRIDE; } return missile; } /* ====================== SnapVectorTowards Round a vector to integers for more efficient network transmission, but make sure that it rounds towards a given point rather than blindly truncating. This prevents it from truncating into a wall. ====================== */ void SnapVectorTowards( vec3_t v, vec3_t to ) { int i; for ( i = 0 ; i < 3 ; i++ ) { if ( to[i] <= v[i] ) { v[i] = (int)v[i]; } else { v[i] = (int)v[i] + 1; } } } /* =============== G_FireWeapon Fires either a bullet or a projectile =============== */ gentity_t* G_FireWeapon( gentity_t *ent, attackType_t attack ) { weaponData_t *weaponDat; attackData_t *attackDat; int projectileLifetime; int flags; weaponDat = &weaponData[ent->s.weapon]; attackDat = &weaponDat->attack[attack]; flags = attackDat->weaponFlags; // Determine lifetime of projectile. Modified by how long we've been holding it! if( !ent->client->ps.grenadeTimer ) { projectileLifetime = attackDat->projectileLifetime; } else { projectileLifetime = ent->client->ps.grenadeTimer; // Less than 50 milliseconds will just cause it to blow up in your hand if ( projectileLifetime < 50 ) { flags &= ~PROJECTILE_TIMED; } } // Clear the grenade timer ent->client->ps.grenadeTimer = 0; if ( attackDat->weaponFlags & PROJECTILE_FIRE) { return G_FireProjectile ( ent, ent->s.weapon, attack, projectileLifetime, flags ); } else { G_FireBullet ( ent, ent->s.weapon, attack ); } return NULL; } /* =============== G_InitHitModel =============== */ void* G_InitHitModel ( void ) { void* ghoul2; char temp[20480]; int numPairs; qhandle_t handle; ghoul2 = NULL; // Initialize the ghoul2 model trap_G2API_InitGhoul2Model ( &ghoul2, "models/characters/average_sleeves/average_sleeves.glm", 0, 0, 0, (1<<4), 2 ); // Verify it if ( !ghoul2 ) { return ghoul2; } // Parse the skin file that will be used for hit information numPairs = BG_ParseSkin ( "models/characters/skins/col_rebel_h1.g2skin", temp, sizeof(temp) ); if ( !numPairs ) { trap_G2API_CleanGhoul2Models ( &ghoul2 ); return NULL; } // Register the skin and attach it to the ghoul model handle = trap_G2API_RegisterSkin( "hitmodel", numPairs, temp ); trap_G2API_SetSkin( ghoul2, 0, handle ); // Read in the animations for this model trap_G2API_GetAnimFileNameIndex( ghoul2, 0, temp ); BG_ParseAnimationFile ( va("%s_mp.cfg", temp), level.ghoulAnimations ); // Hand the hit model back return ghoul2; } /* =============== G_InitHitModels =============== */ void G_InitHitModels ( void ) { level.serverGhoul2 = G_InitHitModel ( ); }