// Copyright (C) 1999-2000 Id Software, Inc. // // g_combat.c #include "g_combat.h" #include "g_local.h" #include "g_breakable.h" //RPG-X | GSIO01 | 09/05/2009: needed by G_Repair #include "g_main.h" #include "g_cmds.h" #include "g_client.h" #include "g_items.h" #include "g_lua.h" #include "g_logger.h" #include "g_missile.h" #include "g_spawn.h" #include "g_syscalls.h" void G_Combat_GibEntity(gentity_t* self, int32_t killer) { G_Assert(self, (void)0); // Start Disintegration G_AddEvent(self, EV_EXPLODESHELL, killer); self->takedamage = qfalse; self->s.eType = ET_INVISIBLE; self->r.contents = 0; } #define BORG_ADAPT_NUM_HITS 10 /** * \brief Checks if borg have adapted to a specified damage type. * \param targ The target. * \param mod The damage type. */ static qboolean G_Combat_CheckBorgAdaptation(gentity_t* targ, int32_t mod) { int32_t weapon = 0; G_Assert(targ, qfalse); G_Assert(targ->client, qfalse); switch (mod) { //other kinds of damage case MOD_UNKNOWN: case MOD_WATER: case MOD_SLIME: case MOD_LAVA: case MOD_CRUSH: case MOD_TELEFRAG: case MOD_FALLING: case MOD_SUICIDE: case MOD_RESPAWN: case MOD_TARGET_LASER: case MOD_TRIGGER_HURT: case MOD_DETPACK: case MOD_MAX: case MOD_KNOCKOUT: case MOD_EXPLOSION: return qfalse; break; // Trek weapons case MOD_PHASER: case MOD_PHASER_ALT: weapon = WP_5; break; case MOD_CRIFLE: case MOD_CRIFLE_SPLASH: case MOD_CRIFLE_ALT: case MOD_CRIFLE_ALT_SPLASH: weapon = WP_6; break; case MOD_SCAVENGER: case MOD_SCAVENGER_ALT: case MOD_SCAVENGER_ALT_SPLASH: case MOD_SEEKER: weapon = WP_4; break; case MOD_STASIS: case MOD_STASIS_ALT: weapon = WP_10; break; case MOD_GRENADE: case MOD_GRENADE_ALT: case MOD_GRENADE_SPLASH: case MOD_GRENADE_ALT_SPLASH: weapon = WP_8; break; case MOD_TETRION: case MOD_TETRION_ALT: weapon = WP_7; break; case MOD_DREADNOUGHT: case MOD_DREADNOUGHT_ALT: weapon = WP_13; break; case MOD_QUANTUM: case MOD_QUANTUM_SPLASH: case MOD_QUANTUM_ALT: case MOD_QUANTUM_ALT_SPLASH: weapon = WP_9; break; case MOD_IMOD: case MOD_IMOD_ALT: weapon = WP_3; break; case MOD_ASSIMILATE: case MOD_BORG: case MOD_BORG_ALT: return qtrue; break; } level.borgAdaptHits[weapon]++; switch (weapon) { case WP_5: if (level.borgAdaptHits[WP_5] > rpg_adaptPhaserHits.integer) return qtrue; break; case WP_6: if (level.borgAdaptHits[WP_6] > rpg_adaptCrifleHits.integer) return qtrue; break; case WP_10: if (level.borgAdaptHits[WP_10] > rpg_adaptDisruptorHits.integer) return qtrue; break; case WP_8: if (level.borgAdaptHits[WP_8] > rpg_adaptGrenadeLauncherHits.integer) return qtrue; break; case WP_7: if (level.borgAdaptHits[WP_7] > rpg_adaptTR116Hits.integer) return qtrue; break; case WP_9: if (level.borgAdaptHits[WP_9] > rpg_adaptPhotonHits.integer) return qtrue; break; default: return qfalse; } return qfalse; } /** * \brief Determines body the location damage was dealt to. * \param point Hit point. * \param targ Target entity. * \param attacker The attacker. * \param take Determines whether the target entity can take damage. */ static int32_t G_Combat_LocationDamage(vec3_t point, gentity_t* targ, gentity_t* attacker, int32_t take) { vec3_t bulletPath = { 0, 0, 0 }; vec3_t bulletAngle = { 0, 0, 0 }; int32_t clientHeight = 0; int32_t clientFeetZ = 0; int32_t clientRotation = 0; int32_t bulletHeight = 0; int32_t bulletRotation = 0; int32_t impactRotation = 0; // First things first. If we're not damaging them, why are we here? if (take == 0) { return 0; } // Point[2] is the REAL world Z. We want Z relative to the clients feet // Where the feet are at [real Z] clientFeetZ = targ->r.currentOrigin[2] + targ->r.mins[2]; // How tall the client is [Relative Z] clientHeight = targ->r.maxs[2] - targ->r.mins[2]; // Where the bullet struck [Relative Z] bulletHeight = point[2] - clientFeetZ; // Get a vector aiming from the client to the bullet hit VectorSubtract(targ->r.currentOrigin, point, bulletPath); // Convert it into PITCH, ROLL, YAW vectoangles(bulletPath, bulletAngle); clientRotation = targ->client->ps.viewangles[YAW]; bulletRotation = bulletAngle[YAW]; impactRotation = abs(clientRotation - bulletRotation); impactRotation += 45; // just to make it easier to work with impactRotation = impactRotation % 360; // Keep it in the 0-359 range if (impactRotation < 90) { targ->client->lasthurt_location = LOCATION_BACK; } else if (impactRotation < 180) { targ->client->lasthurt_location = LOCATION_RIGHT; } else if (impactRotation < 270) { targ->client->lasthurt_location = LOCATION_FRONT; } else if (impactRotation < 360) { targ->client->lasthurt_location = LOCATION_LEFT; } else { targ->client->lasthurt_location = LOCATION_NONE; } // The upper body never changes height, just distance from the feet if (bulletHeight > clientHeight - 2) { targ->client->lasthurt_location |= LOCATION_HEAD; } else if (bulletHeight > clientHeight - 8) { targ->client->lasthurt_location |= LOCATION_FACE; } else if (bulletHeight > clientHeight - 10) { targ->client->lasthurt_location |= LOCATION_SHOULDER; } else if (bulletHeight > clientHeight - 16) { targ->client->lasthurt_location |= LOCATION_CHEST; } else if (bulletHeight > clientHeight - 26) { targ->client->lasthurt_location |= LOCATION_STOMACH; } else if (bulletHeight > clientHeight - 29) { targ->client->lasthurt_location |= LOCATION_GROIN; } else if (bulletHeight < 4) { targ->client->lasthurt_location |= LOCATION_FOOT; } else { // The leg is the only thing that changes size when you duck, // so we check for every other parts RELATIVE location, and // whats left over must be the leg. targ->client->lasthurt_location |= LOCATION_LEG; } // Check the location ignoring the rotation info switch ((targ->client->lasthurt_location & ~(LOCATION_BACK | LOCATION_LEFT | LOCATION_RIGHT | LOCATION_FRONT)) != 0) { case LOCATION_HEAD: take *= 1.8; break; case LOCATION_FACE: if ((targ->client->lasthurt_location & LOCATION_FRONT) != 0) { take *= 5.0; // Faceshots REALLY suck } else { take *= 1.8; } break; case LOCATION_SHOULDER: if ((targ->client->lasthurt_location & (LOCATION_FRONT | LOCATION_BACK)) != 0) { take *= 1.4; // Throat or nape of neck } else { take *= 1.1; // Shoulders } break; case LOCATION_CHEST: if ((targ->client->lasthurt_location & (LOCATION_FRONT | LOCATION_BACK)) != 0) { take *= 1.3; // Belly or back } else { take *= 0.8; // Arms } break; case LOCATION_STOMACH: take *= 1.2; break; case LOCATION_GROIN: if ((targ->client->lasthurt_location & LOCATION_FRONT) != 0) { take *= 1.3; // Groin shot } break; case LOCATION_LEG: take *= 0.7; break; case LOCATION_FOOT: take *= 0.5; break; } return take; } void G_Combat_Damage(gentity_t* targ, gentity_t* inflictor, gentity_t* attacker, vec3_t dir, vec3_t point, int32_t damage, int32_t dflags, int32_t mod) { int32_t take = 0; int32_t knockback = 0; qboolean bFriend = qfalse; gclient_t* client = NULL; G_Assert(targ, (void)0); #ifdef G_LUA if (targ->luaHurt && targ->client == NULL) { LuaHook_G_EntityHurt(targ->luaHurt, targ->s.number, inflictor->s.number, attacker->s.number); } #endif if (!targ->takedamage) { return; } // the intermission has allready been qualified for, so don't // allow any extra scoring if (level.intermissionQueued != 0) { return; } if (inflictor == NULL) { inflictor = &g_entities[ENTITYNUM_WORLD]; } if (attacker == NULL) { attacker = &g_entities[ENTITYNUM_WORLD]; } // shootable doors / buttons don't actually have any health if (((targ->s.eType == ET_MOVER) && (targ->type != ENT_FUNC_BREAKABLE) && (targ->type != ENT_MISC_MODEL_BREAKABLE)) || ((targ->s.eType == ET_MOVER_STR) && (targ->type != ENT_FUNC_BREAKABLE) && (targ->type != ENT_MISC_MODEL_BREAKABLE))) //RPG-X | GSIO01 | 13/05/2009 { if (targ->type == ENT_FUNC_FORCEFIELD) { if (targ->pain != NULL) { targ->pain(targ, inflictor, take); } } else if ((targ->use != NULL) && ((targ->moverState == MOVER_POS1) || (targ->moverState == ROTATOR_POS1)) && (targ->type != ENT_FUNC_DOOR) && (targ->type != ENT_FUNC_DOOR_ROTATING)) { targ->use(targ, inflictor, attacker); } return; } //RPG-X | GSIO01 | 08/05/2009: as we put borg adaption back in we need this again if (rpg_borgAdapt.integer > -1 && G_Combat_CheckBorgAdaptation(targ, mod) && G_Client_IsBorg(targ)) { //flag targ for adaptation effect targ->client->ps.powerups[PW_BORG_ADAPT] = level.time + 250; if (rpg_adaptUseSound.integer == 1) { G_AddEvent(targ, EV_ADAPT_SOUND, 0); } return; } // multiply damage times dmgmult damage *= g_dmgmult.value; // reduce damage by the attacker's handicap value // unless they are rocket jumping if (attacker->client != NULL && attacker != targ) { damage = damage * attacker->client->ps.stats[STAT_MAX_HEALTH] / 100; } client = targ->client; if (client != NULL) { if (client == NULL || client->noclip) { return; } } if (dir == NULL) { 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; } knockback = floor(knockback * g_dmgmult.value); // figure momentum add, even if the damage won't be taken if (knockback && targ->client != NULL) { //if it's non-radius damage knockback from a teammate, don't do it if the damage won't be taken if ((dflags & DAMAGE_ALL_TEAMS) != 0 || (dflags & DAMAGE_RADIUS) != 0 || attacker->client == NULL) { double mass = 200; vec3_t kvel = { 0, 0, 0 }; if (targ->client->ps.powerups[PW_FLIGHT] != 0) { mass *= 0.375; } if (dir != NULL) { VectorScale(dir, g_knockback.value * (double)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 == 0) { int32_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 godmode if ((targ->flags & FL_GODMODE) != 0) { return; } // always give half damage if hurting self // calculated after knockback, so rocket jumping works if (rpg_selfdamage.integer != 0) { if (targ == attacker) { damage *= 0.5; } if (damage < 1) { damage = 1; } } else { if (targ == attacker) { damage *= 0.0; } if (damage < 1) { damage = 0; } } take = damage; // save some from armor //RPG-X: - RedTechie No armor in RPG //asave = CheckArmor (targ, take, dflags); //take -= asave; if (g_debugDamage.integer != 0) { G_Printf("%i: client:%i health:%i damage:%i armor:\n", level.time, targ->s.number, targ->health, take); } // 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 != NULL) { if (attacker != NULL) { client->ps.persistant[PERS_ATTACKER] = attacker->s.number; } else { client->ps.persistant[PERS_ATTACKER] = ENTITYNUM_WORLD; } //RPG-X: - RedTechie no armor in RPG client->damage_blood += take; client->damage_knockback += knockback; if (dir != NULL) { VectorCopy(dir, client->damage_from); client->damage_fromWorld = qfalse; } else { VectorCopy(targ->r.currentOrigin, client->damage_from); client->damage_fromWorld = qtrue; } } if (targ->client != NULL) { // set the last client who damaged the target targ->client->lasthurt_client = attacker->s.number; targ->client->lasthurt_mod = mod; // Modify the damage for location damage if (point != NULL && targ != NULL && targ->health > 1 && attacker != NULL && take != 0) { take = G_Combat_LocationDamage(point, targ, attacker, take); } else { targ->client->lasthurt_location = LOCATION_NONE; } } // do the damage if (take > 0) { // add to the attacker's hit counter if ((MOD_TELEFRAG != mod) && attacker->client != NULL && targ != attacker && targ->health > 0) {//don't telefrag since damage would wrap when sent as a short and the client would think it's a team dmg. if (bFriend) { attacker->client->ps.persistant[PERS_HITS] -= damage; } else if (targ->classname != NULL && strcmp(targ->classname, "holdable_shield") == 0 && strcmp(targ->classname, "holdable_detpack") == 0) { attacker->client->ps.persistant[PERS_HITS] += damage; } } targ->health = targ->health - take; //RPG-X: RedTechie - If medicrevive is on then health only goes down to 1 so we can simulate fake death if ((rpg_medicsrevive.integer == 1) && (targ->type != ENT_FUNC_BREAKABLE) && (targ->type != ENT_MISC_MODEL_BREAKABLE)) { if (targ->health <= 0) { targ->health = 1; } } else { if (rpg_medicsrevive.integer != 1) { if (targ->health == 1) { //RPG-X: RedTechie: Ok regular die now kills the player at 1 health not 0 targ->health = 0; } } } if (targ->client != NULL) { targ->client->ps.stats[STAT_HEALTH] = targ->health; } //RPG-X: RedTechie - Custum medicrevive code if (rpg_medicsrevive.integer == 1 && targ->s.eType == ET_PLAYER) { if (targ->health == 1) { //TiM : Added Client to try and fix this stupid crashy bug client->ps.stats[STAT_WEAPONS] = (1 << WP_0); //?!!!!! client->ps.stats[STAT_HOLDABLE_ITEM] = HI_NONE; targ->health = 1; G_Client_Die(targ, inflictor, attacker, take, mod); } } else { if (targ->health <= 0) { if (client) targ->flags |= FL_NO_KNOCKBACK; if (targ->health < -999) targ->health = -999; #ifdef G_LUA if (targ->luaDie && targ->client == NULL) { LuaHook_G_EntityDie(targ->luaDie, targ->s.number, inflictor->s.number, attacker->s.number, take, mod); } #endif targ->enemy = attacker; targ->die(targ, inflictor, attacker, take, mod); return; } if (targ->pain != NULL) { targ->pain(targ, attacker, take); } } G_LogWeaponDamage(attacker->s.number, mod, take); } } qboolean G_Combat_CanDamage(gentity_t* targ, vec3_t origin) { vec3_t dest = { 0, 0, 0 }; vec3_t midpoint = { 0, 0, 0 }; trace_t tr; // use the midpoint of the bounds instead of the origin, because // bmodels may have their origin is 0,0,0 VectorAdd(targ->r.absmin, targ->r.absmax, midpoint); VectorScale(midpoint, 0.5, midpoint); VectorCopy(midpoint, dest); memset(&tr, 0, sizeof(trace_t)); trap_Trace(&tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); if (tr.fraction == 1.0) { return qtrue; } // this should probably check in the plane of projection, // rather than in world coordinate, and also include Z VectorCopy(midpoint, dest); dest[0] += 15.0; dest[1] += 15.0; memset(&tr, 0, sizeof(trace_t)); trap_Trace(&tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); if (tr.fraction == 1.0) { return qtrue; } VectorCopy(midpoint, dest); dest[0] += 15.0; dest[1] -= 15.0; memset(&tr, 0, sizeof(trace_t)); trap_Trace(&tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); if (tr.fraction == 1.0) { return qtrue; } VectorCopy(midpoint, dest); dest[0] -= 15.0; dest[1] += 15.0; memset(&tr, 0, sizeof(trace_t)); trap_Trace(&tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); if (tr.fraction == 1.0) { return qtrue; } VectorCopy(midpoint, dest); dest[0] -= 15.0; dest[1] -= 15.0; memset(&tr, 0, sizeof(trace_t)); trap_Trace(&tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); if (tr.fraction == 1.0) { return qtrue; } return qfalse; } qboolean G_Combat_RadiusDamage(vec3_t origin, gentity_t* attacker, double damage, double radius, gentity_t* ignore, int32_t dflags, int32_t mod) { double points = 0; double dist = 0; qboolean hitClient = qfalse; vec3_t mins = { 0, 0, 0 }; vec3_t maxs = { 0, 0, 0 }; vec3_t dir = { 0, 0, 0 }; vec3_t v = { 0, 0, 0 }; int32_t entityList[MAX_GENTITIES]; int32_t numListedEntities = 0; int32_t i = 0; int32_t e = 0; gentity_t* ent = NULL; if (radius < 1) { radius = 1; } for (i = 0; i < 3; i++) { mins[i] = origin[i] - radius; maxs[i] = origin[i] + radius; } numListedEntities = trap_EntitiesInBox(mins, maxs, entityList, MAX_GENTITIES); for (e = 0; e < numListedEntities; e++) { ent = &g_entities[entityList[e]]; if (ent == NULL) { continue; } if (ent == ignore) { continue; } if (!ent->takedamage) { continue; } if (ignore != NULL && ignore->parent != NULL && ent->parent == ignore->parent) { if (ignore->think == tripwireThink && ent->think == tripwireThink) {//your own tripwires do not fire off other tripwires of yours. continue; } } // find the distance from the edge of the bounding box for (i = 0; i < 3; i++) { if (origin[i] < ent->r.absmin[i]) { v[i] = ent->r.absmin[i] - origin[i]; } else if (origin[i] > ent->r.absmax[i]) { v[i] = origin[i] - ent->r.absmax[i]; } else { v[i] = 0; } } dist = VectorLength(v); if (dist >= radius) { continue; } points = damage * (1.0 - dist / radius); if (!G_Combat_CanDamage(ent, origin)) { //no LOS to ent if ((dflags & DAMAGE_HALF_NOTLOS) == 0) { //not allowed to do damage without LOS continue; } else { //do 1/2 damage if no LOS but within rad points *= 0.5; } } if (G_Weapon_LogAccuracyHit(ent, attacker)) { hitClient = qtrue; } VectorSubtract(ent->r.currentOrigin, origin, dir); // push the center of mass higher than the origin so players // get knocked into the air more dir[2] += 24; G_Combat_Damage(ent, NULL, attacker, dir, origin, (int)points, dflags | DAMAGE_RADIUS, mod); } return hitClient; } void G_Combat_Repair(gentity_t* ent, gentity_t* tr_ent, double rate) { double distance = 0; double max = 0; vec3_t help = { 0, 0, 0 }; vec3_t forward = { 0, 0, 0 }; int32_t i = 0; // if count isn't 0 the breakable is not damaged and if target is no breakable it does not make sense to go on if ((tr_ent->count != 0) || strstr(tr_ent->classname, "breakable") == 0) { return; } if ((tr_ent->spawnflags & 256) == 0) { // no REPAIRABLE flag set return; } // check if player is near the breakable if ((tr_ent->spawnflags & 512) != 0) { VectorSubtract(tr_ent->s.angles2, ent->r.currentOrigin, help); max = tr_ent->n00bCount; } else { VectorSubtract(tr_ent->s.origin, ent->r.currentOrigin, help); for (i = 0; i < 3; i++) { if (tr_ent->r.maxs[i] > max) { max = tr_ent->r.maxs[i]; } } } distance = VectorLength(help); //G_Printf("goodDst=%f, curDst=%f\n", 80 + max, distance); if (distance > 80 + max) { return; } // check if the player is facing it AngleVectors(ent->client->ps.viewangles, forward, NULL, NULL); if (DotProduct(help, forward) < 0.4) { return; } // check wheter the breakable still needs to be repaired if (tr_ent->health < tr_ent->damage) { // still not repaired of let's go on tr_ent->health += rate; if (tr_ent->health >= tr_ent->damage) {//we're maxed out after this cycle, reenstate tr_ent->health = tr_ent->damage; if (tr_ent->target) { G_UseTargets2(tr_ent, tr_ent, tr_ent->target); } if (tr_ent->type == ENT_FUNC_BREAKABLE) { tr_ent->s.solid = CONTENTS_BODY; trap_SetBrushModel(tr_ent, tr_ent->model); tr_ent->r.svFlags &= ~SVF_NOCLIENT; tr_ent->s.eFlags &= ~EF_NODRAW; InitBBrush(tr_ent); if (tr_ent->health) { tr_ent->takedamage = qtrue; } tr_ent->use = breakable_use; if (tr_ent->paintarget) { tr_ent->pain = breakable_pain; } tr_ent->clipmask = 0; tr_ent->count = 1; } else if (tr_ent->type == ENT_MISC_MODEL_BREAKABLE) { SP_misc_model_breakable(tr_ent); } } } }