/* =========================================================================== Copyright (C) 2024 the OpenMoHAA team This file is part of OpenMoHAA source code. OpenMoHAA source code is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. OpenMoHAA source code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenMoHAA source code; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // weaputils.cpp: General Weapon Utility Functions #include "g_phys.h" #include "g_spawn.h" #include "weaputils.h" #include "specialfx.h" #include "sentient.h" #include "actor.h" #include "decals.h" #include "weapon.h" #include "player.h" #include "VehicleCollisionEntity.h" #include "weapturret.h" #include "vehicleturret.h" #include "earthquake.h" #include "trigger.h" #include "debuglines.h" #include "smokegrenade.h" constexpr unsigned long MAX_TRAVEL_DIST = 16216; static void FlashPlayers(Vector org, float r, float g, float b, float a, float rad, float time, int type); qboolean MeleeAttack( Vector pos, Vector end, float damage, Entity *attacker, meansOfDeath_t means_of_death, float attack_width, float attack_min_height, float attack_max_height, float knockback, qboolean hit_dead, Container *victimlist ) { trace_t trace; Entity *victim; Vector dir; float world_dist; Vector new_pos; Entity *skip_ent; qboolean hit_something = false; Vector mins; Vector maxs; Container potential_victimlist; int i; int num_traces; Vector start; // See how far the world is away dir = end - pos; world_dist = dir.length(); new_pos = pos; skip_ent = attacker; num_traces = 0; while (new_pos != end) { trace = G_Trace(pos, vec_zero, vec_zero, end, skip_ent, MASK_SOLID, false, "MeleeAttack - World test"); num_traces++; if (trace.fraction < 1) { if ((trace.entityNum == ENTITYNUM_WORLD) || (trace.ent && trace.ent->entity && !trace.ent->entity->takedamage)) { dir = trace.endpos - pos; world_dist = dir.length(); break; } else { // Make sure we don't go backwards any in our trace if (Vector(new_pos - pos).length() + 0.001 >= Vector(trace.endpos - pos).length()) { break; } if (num_traces > 10) { // We have done too many traces, stop here dir = trace.endpos - pos; world_dist = dir.length(); break; } new_pos = trace.endpos; if (trace.ent) { skip_ent = trace.ent->entity; } } } else { break; } } // Find things hit dir = end - pos; dir.normalize(); end = pos + (dir * world_dist); start = pos - dir * (attack_width * 1.2f); start.z = pos.z - dir.z * (end.z - start.z); victim = NULL; mins = Vector(-attack_width, -attack_width, attack_min_height); maxs = Vector(attack_width, attack_width, attack_max_height); G_TraceEntities(pos, mins, maxs, end, &potential_victimlist, MASK_MELEE); /*int previous_contents = attacker->edict->r.contents; attacker->edict->r.contents = 0; trace = G_Trace( pos, mins, maxs, end, ( ( Sentient * )attacker )->GetActiveWeapon( WEAPON_MAIN ), MASK_MELEE, false, "MeleeAttack" ); if( trace.ent && trace.ent->entity ) { gi.Printf( "HIT\n" ); potential_victimlist.AddObject( trace.ent->entity ); } attacker->edict->r.contents = previous_contents;*/ for (i = 1; i <= potential_victimlist.NumObjects(); i++) { victim = potential_victimlist.ObjectAt(i); if (victim && victim->takedamage && victim != attacker) { dir = end - pos; dir.normalize(); if (dir == vec_zero) { dir = victim->centroid - pos; dir.normalize(); } if (victim->IsSubclassOfSentient() && !victim->IsDead()) { hit_something = true; } if (victim->health > 0 || hit_dead) { if (victimlist && victim->IsSubclassOfSentient() && !victim->IsDead()) { victimlist->AddObject(victim); } victim->Damage(attacker, attacker, damage, pos, dir, vec_zero, knockback, 0, means_of_death); if (!(victim->edict->r.contents & CONTENTS_CLAYPIDGEON)) { victim->Sound("pistol_hit"); } } } } return hit_something; } #define DEFAULT_SWORD_DAMAGE 10 #define DEFAULT_SWORD_KNOCKBACK 50 Event EV_Projectile_Speed ( "speed", EV_DEFAULT, "f", "projectileSpeed", "set the speed of the projectile", EV_NORMAL ); Event EV_Projectile_MinSpeed ( "minspeed", EV_DEFAULT, "f", "minspeed", "set the minimum speed of the projectile (this is for charge up weapons)", EV_NORMAL ); Event EV_Projectile_ChargeSpeed ( "chargespeed", EV_DEFAULT, NULL, NULL, "set the projectile's speed to be determined by the charge time", EV_NORMAL ); Event EV_Projectile_Damage ( "hitdamage", EV_DEFAULT, "f", "projectileHitDamage", "set the damage a projectile does when it hits something", EV_NORMAL ); Event EV_Projectile_Life ( "life", EV_DEFAULT, "f", "projectileLife", "set the life of the projectile", EV_NORMAL ); Event EV_Projectile_DMLife ( "dmlife", EV_DEFAULT, "f", "projectileLife", "set the life of the projectile in DM", EV_NORMAL ); Event EV_Projectile_MinLife ( "minlife", EV_DEFAULT, "f", "minProjectileLife", "set the minimum life of the projectile (this is for charge up weapons)", EV_NORMAL ); Event EV_Projectile_ChargeLife ( "chargelife", EV_DEFAULT, NULL, NULL, "set the projectile's life to be determined by the charge time", EV_NORMAL ); // Added in 2.0 Event EV_Projectile_SetFuse ( "fuse", EV_DEFAULT, "i", "fuse", "set the projectile's life to be determined by the charge time", EV_NORMAL ); Event EV_Projectile_Knockback ( "knockback", EV_DEFAULT, "f", "projectileKnockback", "set the knockback of the projectile when it hits something", EV_NORMAL ); Event EV_Projectile_DLight ( "dlight", EV_DEFAULT, "ffff", "red green blue intensity", "set the color and intensity of the dynamic light on the projectile", EV_NORMAL ); Event EV_Projectile_Avelocity ( "avelocity", EV_DEFAULT, "SFSFSF", "[random|crandom] yaw [random|crandom] pitch [random|crandom] roll", "set the angular velocity of the projectile", EV_NORMAL ); Event EV_Projectile_MeansOfDeath ( "meansofdeath", EV_DEFAULT, "s", "meansOfDeath", "set the meansOfDeath of the projectile", EV_NORMAL ); Event EV_Projectile_BeamCommand ( "beam", EV_DEFAULT, "sSSSSSS", "command arg1 arg2 arg3 arg4 arg5 arg6", "send a command to the beam of this projectile", EV_NORMAL ); Event EV_Projectile_UpdateBeam ( "updatebeam", EV_DEFAULT, NULL, NULL, "Update the attached beam", EV_NORMAL ); Event EV_Projectile_BounceTouch ( "bouncetouch", EV_DEFAULT, NULL, NULL, "Make the projectile bounce when it hits a non-damageable solid", EV_NORMAL ); Event EV_Projectile_BounceSound ( "bouncesound", EV_DEFAULT, NULL, NULL, "Set the name of the sound that is played when the projectile bounces", EV_NORMAL ); Event EV_Projectile_BounceSound_Metal ( "bouncesound_metal", EV_DEFAULT, NULL, NULL, "Set the name of the sound that is played when the projectile bounces off metal", EV_NORMAL ); Event EV_Projectile_BounceSound_Hard ( "bouncesound_hard", EV_DEFAULT, NULL, NULL, "Set the name of the sound that is played when the projectile bounces off hard surfaces", EV_NORMAL ); Event EV_Projectile_BounceSound_Water ( "bouncesound_water", EV_DEFAULT, NULL, NULL, "Set the name of the sound that is played when the projectile bounces off water", EV_NORMAL ); Event EV_Projectile_Explode ( "explode", EV_DEFAULT, NULL, NULL, "Make the projectile explode", EV_NORMAL ); Event EV_Projectile_ImpactMarkShader ( "impactmarkshader", EV_DEFAULT, "s", "shader", "Set the impact mark of the shader", EV_NORMAL ); Event EV_Projectile_ImpactMarkRadius ( "impactmarkradius", EV_DEFAULT, "f", "radius", "Set the radius of the impact mark", EV_NORMAL ); Event EV_Projectile_ImpactMarkOrientation ( "impactmarkorientation", EV_DEFAULT, "f", "degrees", "Set the orientation of the impact mark", EV_NORMAL ); Event EV_Projectile_SetExplosionModel ( "explosionmodel", EV_DEFAULT, "s", "modelname", "Set the modelname of the explosion to be spawned", EV_NORMAL ); Event EV_Projectile_SetAddVelocity ( "addvelocity", EV_DEFAULT, "fff", "velocity_x velocity_y velocity_z", "Set a velocity to be added to the projectile when it is created", EV_NORMAL ); Event EV_Projectile_AddOwnerVelocity ( "addownervelocity", EV_DEFAULT, "b", "bool", "Set whether or not the owner's velocity is added to the projectile's velocity", EV_NORMAL ); Event EV_Projectile_HeatSeek ( "heatseek", EV_DEFAULT, NULL, NULL, "Make the projectile heat seek", EV_NORMAL ); Event EV_Projectile_Drunk ( "drunk", EV_DEFAULT, "ff", "amount rate", "Make the projectile drunk", EV_NORMAL ); Event EV_Projectile_Prethink ( "prethink", EV_DEFAULT, NULL, NULL, "Make the projectile think to update it's velocity", EV_NORMAL ); Event EV_Projectile_SetCanHitOwner ( "canhitowner", EV_DEFAULT, NULL, NULL, "Make the projectile be able to hit its owner", EV_NORMAL ); Event EV_Projectile_ClearOwner ( "clearowner", EV_DEFAULT, NULL, NULL, "Make the projectile be able to hit its owner now", EV_NORMAL ); Event EV_Projectile_RemoveWhenStopped ( "removewhenstopped", EV_DEFAULT, NULL, NULL, "Make the projectile get removed when it stops", EV_NORMAL ); Event EV_Projectile_ExplodeOnTouch ( "explodeontouch", EV_DEFAULT, NULL, NULL, "Make the projectile explode when it touches something damagable", EV_NORMAL ); Event EV_Projectile_NoTouchDamage ( "notouchdamage", EV_DEFAULT, NULL, NULL, "Makes the projectile not blow up or deal damage when it touches a damagable object", EV_NORMAL ); Event EV_Projectile_SetSmashThroughGlass ( "smashthroughglass", EV_DEFAULT, "i", "speed", "Makes the projectile smash through windows & other damageble glass objects when going above a set speed", EV_NORMAL ); Event EV_Projectile_SmashThroughGlass ( "_smashthroughglass", EV_DEFAULT, NULL, NULL, "Think function for smashing through glass", EV_NORMAL ); // Added in 2.0 Event EV_Projectile_ArcToTarget ( "arctotarget", EV_DEFAULT, NULL, NULL, "Make the projectile follow a normal arc on its way to its target", EV_NORMAL ); // Added in 2.0 Event EV_Projectile_BecomeBomb ( "becomebomb", EV_DEFAULT, NULL, NULL, "Make the projectile into a bomb", EV_NORMAL ); // Added in 2.30 Event EV_Projectile_DieInWater ( "dieinwater", EV_DEFAULT, NULL, NULL, "Make the projectile die when gets wet", EV_NORMAL ); CLASS_DECLARATION(Animate, Projectile, NULL) { {&EV_Touch, &Projectile::Touch }, {&EV_Projectile_Speed, &Projectile::SetSpeed }, {&EV_Projectile_MinSpeed, &Projectile::SetMinSpeed }, {&EV_Projectile_ChargeSpeed, &Projectile::SetChargeSpeed }, {&EV_Projectile_Damage, &Projectile::SetDamage }, {&EV_Projectile_Life, &Projectile::SetLife }, {&EV_Projectile_DMLife, &Projectile::SetDMLife }, {&EV_Projectile_MinLife, &Projectile::SetMinLife }, {&EV_Projectile_ChargeLife, &Projectile::SetChargeLife }, {&EV_Projectile_SetFuse, &Projectile::SetFuse }, {&EV_Projectile_Knockback, &Projectile::SetKnockback }, {&EV_Projectile_DLight, &Projectile::SetDLight }, {&EV_Projectile_Avelocity, &Projectile::SetAvelocity }, {&EV_Projectile_MeansOfDeath, &Projectile::SetMeansOfDeath }, {&EV_Projectile_BounceTouch, &Projectile::SetBounceTouch }, {&EV_Projectile_BounceSound, &Projectile::SetBounceSound }, {&EV_Projectile_BounceSound_Metal, &Projectile::SetBounceSoundMetal }, {&EV_Projectile_BounceSound_Hard, &Projectile::SetBounceSoundHard }, {&EV_Projectile_BounceSound_Water, &Projectile::SetBounceSoundWater }, {&EV_Projectile_BeamCommand, &Projectile::BeamCommand }, {&EV_Projectile_UpdateBeam, &Projectile::UpdateBeam }, {&EV_Projectile_Explode, &Projectile::Explode }, {&EV_Projectile_ImpactMarkShader, &Projectile::SetImpactMarkShader }, {&EV_Projectile_ImpactMarkRadius, &Projectile::SetImpactMarkRadius }, {&EV_Projectile_ImpactMarkOrientation, &Projectile::SetImpactMarkOrientation}, {&EV_Projectile_SetExplosionModel, &Projectile::SetExplosionModel }, {&EV_Projectile_SetAddVelocity, &Projectile::SetAddVelocity }, {&EV_Projectile_AddOwnerVelocity, &Projectile::AddOwnerVelocity }, {&EV_Projectile_HeatSeek, &Projectile::HeatSeek }, {&EV_Projectile_Drunk, &Projectile::Drunk }, {&EV_Projectile_Prethink, &Projectile::Prethink }, {&EV_Projectile_SetCanHitOwner, &Projectile::SetCanHitOwner }, {&EV_Projectile_ClearOwner, &Projectile::ClearOwner }, {&EV_Projectile_RemoveWhenStopped, &Projectile::RemoveWhenStopped }, {&EV_Projectile_ExplodeOnTouch, &Projectile::ExplodeOnTouch }, {&EV_Projectile_NoTouchDamage, &Projectile::SetNoTouchDamage }, {&EV_Projectile_SetSmashThroughGlass, &Projectile::SetSmashThroughGlass }, {&EV_Projectile_SmashThroughGlass, &Projectile::SmashThroughGlassThink }, {&EV_Projectile_BecomeBomb, &Projectile::BecomeBomb }, {&EV_Killed, &Projectile::Explode }, {&EV_Stop, &Projectile::Stopped }, {&EV_Projectile_ArcToTarget, &Projectile::ArcToTarget }, {&EV_Projectile_DieInWater, &Projectile::DieInWater }, {NULL, NULL } }; Projectile::Projectile() { entflags |= ECF_PROJECTILE; if (LoadingSavegame) { // Archive function will setup all necessary data return; } minlife = 0; m_beam = NULL; speed = 0; minspeed = 0; damage = 0; life = 5; dmlife = 0; knockback = 0; dlight_radius = 0; dlight_color = Vector(1, 1, 1); avelocity = Vector(0, 0, 0); mins = Vector(-1, -1, -1); maxs = Vector(1, 1, 1); meansofdeath = MOD_NONE; projFlags = 0; fLastBounceTime = 0; gravity = 0; impactmarkradius = 10; charge_fraction = 1.0; target = NULL; addownervelocity = false; fDrunk = 0; fDrunkRate = 0; can_hit_owner = false; remove_when_stopped = false; m_bExplodeOnTouch = false; m_bHurtOwnerOnly = false; m_iSmashThroughGlass = 0; takedamage = DAMAGE_NO; owner = ENTITYNUM_NONE; edict->r.ownerNum = ENTITYNUM_NONE; // Added in 2.0 m_bArcToTarget = false; // Added in 2.30 m_bDieInWater = false; // make this shootable but non-solid on the client setContents(CONTENTS_SHOOTONLY); // // touch triggers by default // flags |= FL_TOUCH_TRIGGERS; m_iTeam = 0; // Added in OPM m_bHadPlayerOwner = false; } float Projectile::ResolveMinimumDistance(Entity *potential_target, float currmin) { float currdist; float dot; Vector angle; Vector delta; Vector norm; float sine = 0.4f; delta = potential_target->centroid - this->origin; norm = delta; norm.normalize(); // Test if the target is in front of the missile dot = norm * orientation[0]; if (dot < 0) { return currmin; } // Test if we're within the rocket's viewcone (45 degree cone) dot = norm * orientation[1]; if (fabs(dot) > sine) { return currmin; } dot = norm * orientation[2]; if (fabs(dot) > sine) { return currmin; } currdist = delta.length(); if (currdist < currmin) { currmin = currdist; target = potential_target; } return currmin; } float Projectile::AdjustAngle(float maxadjust, float currangle, float targetangle) { float dangle; float magangle; dangle = currangle - targetangle; if (dangle) { magangle = (float)fabs(dangle); while (magangle >= 360.0f) { magangle -= 360.0f; } if (magangle < maxadjust) { currangle = targetangle; } else { if (magangle > 180.0f) { maxadjust = -maxadjust; } if (dangle > 0) { maxadjust = -maxadjust; } currangle += maxadjust; } } while (currangle >= 360.0f) { currangle -= 360.0f; } while (currangle < 0.0f) { currangle += 360.0f; } return currangle; } void Projectile::Drunk(Event *ev) { if (fDrunk) { return; } fDrunk = ev->GetFloat(1); fDrunkRate = ev->GetFloat(2); PostEvent(EV_Projectile_Prethink, 0); } void Projectile::HeatSeek(Event *ev) { float mindist; Entity *ent; trace_t trace; Vector delta; Vector v; int n; int i; if ((!target) || (target == world)) { mindist = 8192.0f; n = SentientList.NumObjects(); for (i = 1; i <= n; i++) { ent = SentientList.ObjectAt(i); if (ent->entnum == owner) { continue; } if (((ent->takedamage != DAMAGE_AIM) || (ent->health <= 0))) { continue; } trace = G_Trace( this->origin, vec_zero, vec_zero, ent->centroid, this, MASK_SHOT, qfalse, "DrunkMissile::HeatSeek" ); if ((trace.fraction != 1.0) && (trace.ent != ent->edict)) { continue; } mindist = ResolveMinimumDistance(ent, mindist); } } else { float angspeed; delta = target->centroid - this->origin; v = delta.toAngles(); angspeed = 5.0f; angles.x = AdjustAngle(angspeed, angles.x, v.x); angles.y = AdjustAngle(angspeed, angles.y, v.y); angles.z = AdjustAngle(angspeed, angles.z, v.z); } PostEvent(EV_Projectile_HeatSeek, 0.1f); PostEvent(EV_Projectile_Prethink, 0); } void Projectile::Prethink(Event *ev) { Vector end; if (fDrunk >= 0.0f) { if (fDrunk) { float rnd1 = G_Random(); float rnd2 = G_Random(); angles += Vector((rnd1 + rnd1 - 1.0f) * fDrunk, (rnd2 + rnd2 - 1.0f) * fDrunk, 0); speed *= 0.98f; fDrunk += fDrunkRate; if (speed < 500.0f) { fDrunk = -fDrunk; } } setAngles(angles); velocity = Vector(orientation[0]) * speed; } else { fDrunk *= 0.80f; if (fDrunk > -1.0f) { fDrunk = -1.0f; } //float rnd1 = rand() & 0x7FFF; //float rnd2 = rand() & 0x7FFF; //angles += Vector( ( rnd1 * 0.00003f + rnd1 * 0.00003f - 1.0f ) * fDrunk, ( rnd2 * 0.00003f + rnd2 * 0.00003f - 1.0f ) * fDrunk, 0 ); velocity[2] -= 0.05f * sv_gravity->integer * 0.15f; angles = velocity.toAngles(); setAngles(angles); } PostEvent(EV_Projectile_Prethink, 0.05f); } void Projectile::AddOwnerVelocity(Event *ev) { addownervelocity = ev->GetBoolean(1); } void Projectile::SetAddVelocity(Event *ev) { addvelocity.x = ev->GetFloat(1); addvelocity.y = ev->GetFloat(2); addvelocity.z = ev->GetFloat(3); } void Projectile::SetExplosionModel(Event *ev) { explosionmodel = ev->GetString(1); } void Projectile::SetImpactMarkShader(Event *ev) { impactmarkshader = ev->GetString(1); } void Projectile::SetImpactMarkRadius(Event *ev) { impactmarkradius = ev->GetFloat(1); } void Projectile::SetImpactMarkOrientation(Event *ev) { impactmarkorientation = ev->GetString(1); } void Projectile::Explode(Event *ev) { Entity *owner; Entity *ignoreEnt = NULL; if (!CheckTeams()) { PostEvent(EV_Remove, EV_REMOVE); return; } if (ev->NumArgs() == 1) { ignoreEnt = ev->GetEntity(1); } // Get the owner of this projectile owner = G_GetEntity(this->owner); // If the owner's not here, make the world the owner if (!owner) { owner = world; } if (owner->IsDead() || owner == world) { weap = NULL; } health = 0; deadflag = DEAD_DEAD; takedamage = DAMAGE_NO; // Spawn an explosion model if (explosionmodel.length()) { // Move the projectile back off the surface a bit so we can see // explosion effects. Vector dir, v; dir = velocity; if (dir.normalize() == 0) { vec3_t forward; AngleVectors(angles, forward, NULL, NULL); dir = forward; } v = origin; ExplosionAttack(v, owner, explosionmodel, dir, ignoreEnt, 1.0f, weap, m_bHurtOwnerOnly); } CancelEventsOfType(EV_Projectile_UpdateBeam); // Kill the beam if (m_beam) { m_beam->ProcessEvent(EV_Remove); m_beam = NULL; } // Remove the projectile PostEvent(EV_Remove, level.frametime); } void Projectile::SetBounceTouch(Event *ev) { projFlags |= P_BOUNCE_TOUCH; setMoveType(MOVETYPE_BOUNCE); } void Projectile::SetNoTouchDamage(Event *ev) { projFlags |= P_NO_TOUCH_DAMAGE; } void Projectile::SetSmashThroughGlass(Event *ev) { m_iSmashThroughGlass = ev->GetInteger(1); PostEvent(EV_Projectile_SmashThroughGlass, level.frametime); } void Projectile::SmashThroughGlassThink(Event *ev) { if (velocity.length() > m_iSmashThroughGlass) { Vector vEnd; trace_t trace; Entity *ent = G_GetEntity(owner); vEnd = velocity * level.frametime + velocity * level.frametime + origin; trace = G_Trace(origin, vec_zero, vec_zero, vEnd, ent, MASK_BEAM, false, "SmashThroughGlassThink"); if ((trace.ent) && (trace.ent->entity != world)) { Entity *obj = trace.ent->entity; if ((trace.surfaceFlags & SURF_GLASS) && (obj->takedamage)) { obj->Damage( this, ent, obj->health + 1, origin, velocity, trace.plane.normal, knockback, 0, meansofdeath ); } } } PostEvent(EV_Projectile_SmashThroughGlass, level.frametime); } void Projectile::BeamCommand(Event *ev) { int i; if (!m_beam) { m_beam = new FuncBeam; m_beam->setOrigin(this->origin); m_beam->Ghost(NULL); } Event *beamev = new Event(ev->GetToken(1)); for (i = 2; i <= ev->NumArgs(); i++) { beamev->AddToken(ev->GetToken(i)); } m_beam->ProcessEvent(beamev); PostEvent(EV_Projectile_UpdateBeam, level.frametime); } void Projectile::UpdateBeam(Event *ev) { if (m_beam) { m_beam->setOrigin(this->origin); PostEvent(EV_Projectile_UpdateBeam, level.frametime); } } void Projectile::SetBounceSound(Event *ev) { bouncesound = ev->GetString(1); } void Projectile::SetBounceSoundMetal(Event *ev) { bouncesound_metal = ev->GetString(1); } void Projectile::SetBounceSoundHard(Event *ev) { bouncesound_hard = ev->GetString(1); } void Projectile::SetBounceSoundWater(Event *ev) { bouncesound_water = ev->GetString(1); } void Projectile::SetChargeLife(Event *ev) { if (ev->NumArgs() <= 0 || ev->GetInteger(1)) { projFlags |= P_CHARGE_LIFE; } else { // Added in 2.0 // Allow disabling the charge life when set to 0 projFlags &= ~P_CHARGE_LIFE; } } void Projectile::SetFuse(Event *ev) { if (ev->GetInteger(1)) { projFlags |= P_FUSE; } else { projFlags &= ~P_FUSE; } } void Projectile::SetMinLife(Event *ev) { minlife = ev->GetFloat(1); if (minlife > 0) { projFlags |= P_CHARGE_LIFE; } else { // Added in 2.0 // Allow disabling the charge life when set to 0 projFlags &= ~P_CHARGE_LIFE; } } void Projectile::SetLife(Event *ev) { life = ev->GetFloat(1); } void Projectile::SetDMLife(Event *ev) { dmlife = ev->GetFloat(1); } void Projectile::SetSpeed(Event *ev) { speed = ev->GetFloat(1); } void Projectile::SetMinSpeed(Event *ev) { minspeed = ev->GetFloat(1); projFlags |= P_CHARGE_SPEED; } void Projectile::SetChargeSpeed(Event *ev) { projFlags |= P_CHARGE_SPEED; } void Projectile::SetAvelocity(Event *ev) { int i = 1; int j = 0; str vel; if (ev->NumArgs() < 3) { warning("ClientGameCommandManager::SetAngularVelocity", "Expecting at least 3 args for command randvel"); } while (j < 3) { vel = ev->GetString(i++); if (vel == "crandom") { avelocity[j++] = ev->GetFloat(i++) * crandom(); } else if (vel == "random") { avelocity[j++] = ev->GetFloat(i++) * random(); } else { avelocity[j++] = atof(vel.c_str()); } } } void Projectile::SetDamage(Event *ev) { damage = ev->GetFloat(1); } void Projectile::SetKnockback(Event *ev) { knockback = ev->GetFloat(1); } void Projectile::SetDLight(Event *ev) { dlight_color[0] = ev->GetFloat(1); dlight_color[1] = ev->GetFloat(2); dlight_color[2] = ev->GetFloat(3); dlight_radius = ev->GetFloat(4); } void Projectile::SetMeansOfDeath(Event *ev) { meansofdeath = (meansOfDeath_t)MOD_string_to_int(ev->GetString(1)); } void Projectile::DoDecal(void) { if (impactmarkshader.length()) { Decal *decal = new Decal; decal->setShader(impactmarkshader); decal->setOrigin(level.impact_trace.endpos); decal->setDirection(level.impact_trace.plane.normal); decal->setOrientation(impactmarkorientation); decal->setRadius(impactmarkradius); } } void Projectile::Touch(Event *ev) { Entity *other; Entity *owner; str realname; // Other is what got hit other = ev->GetEntity(1); assert(other); // Don't touch teleporters if (other->isSubclassOf(Teleporter)) { return; } // Can't hit yourself with a projectile if (other->entnum == this->owner) { return; } // Remove it if we hit the sky if (HitSky()) { PostEvent(EV_Remove, 0); return; } if (!CheckTeams()) { PostEvent(EV_Remove, EV_REMOVE); return; } // Bouncy Projectile if ((projFlags & P_BOUNCE_TOUCH)) { str snd; int flags; if (level.time - fLastBounceTime < 0.1f) { fLastBounceTime = level.time; return; } if (level.impact_trace.surfaceFlags & SURF_PUDDLE || (gi.pointcontents(level.impact_trace.endpos, 0) & (CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA))) { if (bouncesound_water.length()) { this->Sound(bouncesound_water, CHAN_BODY); } // // Added in 2.30 // if (m_bDieInWater) { PostEvent(EV_Remove, 0.5f); } return; } if (bouncesound_metal.length()) { snd = bouncesound_metal; } else if (bouncesound_hard.length()) { snd = bouncesound_hard; } else { snd = bouncesound; } flags = level.impact_trace.surfaceFlags; if (flags & SURF_MUD) { if (bouncesound.length()) { Sound(bouncesound, CHAN_BODY); } } else if (flags & SURF_ROCK) { if (bouncesound_hard.length()) { Sound(bouncesound_hard, CHAN_BODY); } } else if (flags & SURF_GRILL) { if (bouncesound_metal.length()) { Sound(bouncesound_metal, CHAN_BODY); } } else if (flags & SURF_WOOD) { if (bouncesound_hard.length()) { Sound(bouncesound_hard, CHAN_BODY); } } else if (flags & SURF_METAL) { if (bouncesound_metal.length()) { Sound(bouncesound_metal, CHAN_BODY); } } else if (flags & SURF_GLASS) { if (bouncesound_hard.length()) { Sound(bouncesound_hard, CHAN_BODY); } } else { if (bouncesound.length()) { Sound(bouncesound, CHAN_BODY); } } BroadcastAIEvent(AI_EVENT_WEAPON_IMPACT); fLastBounceTime = level.time; return; } if (!m_bExplodeOnTouch && damage == 0.0f) { return; } // Get the owner of this projectile owner = G_GetEntity(this->owner); // If the owner's not here, make the world the owner if (!owner) { owner = world; } if (owner->IsDead() || owner == world) { weap = NULL; } // Damage the thing that got hit if (other->takedamage) { other->Damage( this, owner, damage, origin, velocity, level.impact_trace.plane.normal, knockback, 0, meansofdeath ); } if (!g_gametype->integer && weap) { if (other->IsSubclassOfPlayer() || other->IsSubclassOfVehicle() || other->IsSubclassOfVehicleTank() || other->isSubclassOf(VehicleCollisionEntity)) { weap->m_iNumHits++; weap->m_iNumTorsoShots++; if (weap->IsSubclassOfVehicleTurretGun()) { VehicleTurretGun* t = (VehicleTurretGun*)weap.Pointer(); Player* p = (Player*)t->GetRemoteOwner().Pointer(); if (p && p->IsSubclassOfPlayer()) { p->m_iNumHits++; p->m_iNumTorsoShots++; } } } } if (!m_bExplodeOnTouch) { return; } // Make the projectile not solid setSolidType(SOLID_NOT); setMoveType(MOVETYPE_NONE); hideModel(); // Do a decal DoDecal(); BroadcastAIEvent(AI_EVENT_WEAPON_FIRE); // Remove the projectile PostEvent(EV_Remove, 0); // Call the explosion event Event* explEv; explEv = new Event(EV_Projectile_Explode); explEv->AddEntity(other); ProcessEvent(explEv); // // Added in 2.30 // Check for projectile vulnerability // Vehicle* pVehicle = NULL; if (other->IsSubclassOfVehicle()) { pVehicle = static_cast(other); } else if (other->IsSubclassOfVehicleTurretGun()) { VehicleTurretGun* pTurret = static_cast(other); Entity* pEnt = pTurret->GetVehicle(); if (pEnt && pEnt->IsSubclassOfVehicle()) { pVehicle = static_cast(pEnt); } } else if (other->isSubclassOf(VehicleCollisionEntity)) { VehicleCollisionEntity* pCollision = static_cast(other); if (pCollision && pCollision->GetOwner()->IsSubclassOfVehicle()) { pVehicle = static_cast(pCollision->GetOwner()); } } if (pVehicle && pVehicle->GetProjectileHitsRemaining() > 0) { pVehicle->DoProjectileVulnerability(this, owner, meansofdeath); } } void Projectile::SetCanHitOwner(Event *ev) { can_hit_owner = true; } void Projectile::ClearOwner(Event *ev) { this->owner = ENTITYNUM_NONE; edict->r.ownerNum = ENTITYNUM_NONE; } void Projectile::RemoveWhenStopped(Event *ev) { remove_when_stopped = true; } void Projectile::ExplodeOnTouch(Event *ev) { m_bExplodeOnTouch = true; } Sentient *Projectile::GetOwner(void) { Sentient *pOwner = (Sentient *)G_GetEntity(owner); if (!pOwner || !pOwner->IsSubclassOfSentient()) { return NULL; } else { return pOwner; } } void Projectile::SetOwner(Entity *owner) { if (owner) { this->owner = owner->entnum; edict->r.ownerNum = owner->entnum; if (owner->IsSubclassOfPlayer()) { Player *p = (Player *)owner; m_iTeam = p->GetTeam(); // Added in OPM // this was added to prevent glitches, like when the player // disconnects or when the player spectates m_bHadPlayerOwner = true; } m_pOwnerPtr = owner; } } void Projectile::ArcToTarget(Event *ev) { m_bArcToTarget = true; PostEvent(EV_Projectile_Prethink, 0); } void Projectile::BecomeBomb(Event *ev) { if (ev->NumArgs() > 0) { Entity *ent = ev->GetEntity(1); if (ent) { owner = ent->entnum; setOrigin(ent->origin - Vector(0, 0, 48)); setAngles(ent->angles); velocity = ent->velocity; } } m_bExplodeOnTouch = true; gravity = 1.f; setMoveType(MOVETYPE_BOUNCE); setSolidType(SOLID_BBOX); edict->clipmask = MASK_PROJECTILE; setSize(mins, maxs); } void Projectile::DieInWater(Event *ev) { m_bDieInWater = true; } void Projectile::Stopped(Event *ev) { if (remove_when_stopped) { PostEvent(EV_Remove, 0); } } void Projectile::Think() { float angle, spin; avelocity.x *= 1.3f; angle = AngleNormalize180(angles.x); spin = avelocity.x * level.frametime + angle; if ((angle >= 0 || spin <= 0) && (angle <= 0 || spin >= 0)) { angle = AngleNormalize360(angles.x); spin = avelocity.x * level.frametime + angle; if ((angle < 180 && spin > 180) || (angle > 180 && spin < 180)) { setAngles(Vector(0, angles.y + 180, angles.z)); avelocity = vec_zero; flags &= ~FL_THINK; } } else { setAngles(Vector(0, angles.y, angles.z)); avelocity = vec_zero; flags &= ~FL_THINK; } } bool Projectile::CheckTeams(void) { Player *pOwner; if (g_gametype->integer == GT_SINGLE_PLAYER) { // Ignore in single-player mode return true; } pOwner = (Player *)m_pOwnerPtr.Pointer(); if (!pOwner) { // Owner disconnected if (m_bHadPlayerOwner) { return false; } else { return true; } } if (pOwner->IsSubclassOfPlayer()) { if (m_iTeam != TEAM_NONE && m_iTeam != pOwner->GetTeam()) { return false; } } else if (pOwner->IsSubclassOfSentient()) { if (m_iTeam != pOwner->m_Team) { return false; } } return true; } Event EV_Explosion_Radius ( "radius", EV_DEFAULT, "f", "projectileRadius", "set the radius for the explosion", EV_NORMAL ); Event EV_Explosion_ConstantDamage ( "constantdamage", EV_DEFAULT, NULL, NULL, "Makes the explosion do constant damage over the radius", EV_NORMAL ); Event EV_Explosion_DamageEveryFrame ( "damageeveryframe", EV_DEFAULT, NULL, NULL, "Makes the explosion damage every frame", EV_NORMAL ); Event EV_Explosion_DamageAgain ( "damageagain", EV_DEFAULT, NULL, NULL, "This event is generated each frame if explosion is set to damage each frame", EV_NORMAL ); Event EV_Explosion_Flash ( "flash", EV_DEFAULT, "fffff", "time r g b radius", "Flash player screens", EV_NORMAL ); Event EV_Explosion_RadiusDamage ( "radiusdamage", EV_DEFAULT, "f", "radiusDamage", "set the radius damage an explosion does", EV_NORMAL ); Event EV_Explosion_Effect ( "explosioneffect", EV_DEFAULT, "s", "explosiontype", "Make an explosionType explosion effect", EV_NORMAL ); CLASS_DECLARATION(Projectile, Explosion, NULL) { {&EV_Explosion_Radius, &Explosion::SetRadius }, {&EV_Explosion_ConstantDamage, &Explosion::SetConstantDamage }, {&EV_Explosion_DamageEveryFrame, &Explosion::SetDamageEveryFrame}, {&EV_Explosion_DamageAgain, &Explosion::DamageAgain }, {&EV_Explosion_Flash, &Explosion::SetFlash }, {&EV_Explosion_RadiusDamage, &Explosion::SetRadiusDamage }, {&EV_Explosion_Effect, &Explosion::MakeExplosionEffect}, {NULL, NULL } }; Explosion::Explosion() { if (LoadingSavegame) { // Archive function will setup all necessary data return; } flash_r = 0; flash_g = 0; flash_b = 0; flash_a = 0; flash_radius = 0; radius = 0; constant_damage = false; damage_every_frame = false; flash_time = 0; flash_type = 0; owner = ENTITYNUM_NONE; hurtOwnerOnly = false; } void Explosion::SetFlash(Event *ev) { flash_time = ev->GetFloat(1); flash_r = ev->GetFloat(2); flash_g = ev->GetFloat(3); flash_b = ev->GetFloat(4); flash_a = ev->GetFloat(5); flash_radius = ev->GetFloat(6); flash_type = 0; if (ev->NumArgs() > 6) { str t = ev->GetString(7); if (!t.icmp("addblend")) { flash_type = 1; } else if (!t.icmp("alphablend")) { flash_type = 0; } } } void Explosion::SetRadius(Event *ev) { radius = ev->GetFloat(1); } void Explosion::SetRadiusDamage(Event *ev) { radius_damage = ev->GetFloat(1); } void Explosion::SetConstantDamage(Event *ev) { constant_damage = true; } void Explosion::SetDamageEveryFrame(Event *ev) { damage_every_frame = true; } void Explosion::DamageAgain(Event *ev) { Entity *owner_ent; Weapon *weapon; float dmg; float rad; if (!CheckTeams()) { PostEvent(EV_Remove, EV_REMOVE); return; } owner_ent = G_GetEntity(owner); if (!owner_ent) { owner_ent = world; } weapon = weap; if (owner_ent == world || owner_ent->IsDead()) { weap = NULL; } dmg = radius_damage; rad = radius; if (rad == 0.0f) { rad = radius_damage + 60.0f; } RadiusDamage(origin, this, owner_ent, dmg, NULL, meansofdeath, rad, knockback, constant_damage, weapon, false); PostEvent(EV_Explosion_DamageAgain, level.frametime); } void Explosion::MakeExplosionEffect(Event *ev) { str sEffect = ev->GetString(1); gi.SetBroadcastAll(); if (!sEffect.icmp("grenade")) { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_EXPLOSION_EFFECT_1)); } else { if (g_protocol >= protocol_e::PROTOCOL_MOHTA_MIN) { if (!sEffect.icmp("heavyshell")) { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_EXPLOSION_EFFECT_3)); } else if (!sEffect.icmp("tank")) { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_EXPLOSION_EFFECT_4)); } else { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_EXPLOSION_EFFECT_2)); } } else { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_EXPLOSION_EFFECT_2)); } } gi.MSG_WriteCoord(origin[0]); gi.MSG_WriteCoord(origin[1]); gi.MSG_WriteCoord(origin[2]); gi.MSG_EndCGM(); } Entity *FindDefusableObject(const Vector& dir, Entity *owner, float maxdist) { Vector startOrg; Vector endOrg; Vector mins, maxs; float fCos; int numAreaEntities; int i; int entNums[MAX_GENTITIES]; startOrg = owner->origin; endOrg = startOrg + dir * maxdist; if (startOrg.z > endOrg.z) { endOrg.z -= 10; startOrg.z += 10; } else { startOrg.z -= 10; endOrg.z += 10; } fCos = cos(M_PI / 3); for (i = 0; i < 3; i++) { if (endOrg[i] > startOrg[i]) { mins[i] = startOrg[i] - 1; maxs[i] = endOrg[i] + 1; } else { mins[i] = endOrg[i] - 1; maxs[i] = startOrg[i] + 1; } } if (endOrg.x > startOrg.x) { mins[0] = startOrg.x - 1; maxs[0] = endOrg.x + 1; } else { mins[0] = endOrg.x - 1; maxs[0] = startOrg.x + 1; } numAreaEntities = gi.AreaEntities(mins, maxs, entNums, ARRAY_LEN(entNums)); for (i = 0; i < numAreaEntities; i++) { gentity_t *pgEnt = &g_entities[entNums[i]]; Entity *pEnt; vec3_t delta; if (!pgEnt->solid) { continue; } pEnt = pgEnt->entity; VectorSubtract(pEnt->centroid, startOrg, delta); VectorNormalize(delta); if (pEnt == owner) { continue; } if (DotProduct(delta, dir) > fCos && pEnt->Vars()->VariableExists("defuseThread")) { return pEnt; } } return NULL; } void DefuseObject(const Vector& dir, Entity *owner, float maxdist) { Entity *defusableObj = FindDefusableObject(dir, owner, maxdist); ScriptVariable *defuseThreadVar; str defuseThreadName; if (!defusableObj) { return; } defuseThreadVar = defusableObj->Vars()->GetVariable("defuseThread"); if (defuseThreadVar) { defuseThreadName = defuseThreadVar->stringValue(); } ScriptThreadLabel label; label.Set(defuseThreadName); label.Execute(defusableObj, owner, ListenerPtr()); } qboolean CanPlaceLandmine(const Vector& origin, Entity *owner) { Vector vEnd; Vector vDelta; trace_t trace; if (!level.RoundStarted()) { gi.DPrintf("Can't place landmine until the round starts\n"); return qfalse; } vEnd = origin - Vector(0, 0, 256); trace = G_Trace(origin, vec_zero, vec_zero, vEnd, owner, MASK_LANDMINE_PLACE, qfalse, "LandminePlace", qtrue); vDelta = origin - trace.endpos; if (vDelta.length() > 90) { gi.DPrintf("Too high to place landmine\n"); return qfalse; } if (trace.surfaceFlags & SURF_WOOD) { gi.DPrintf("Can't place landmine on wood\n"); return qfalse; } else if (trace.surfaceFlags & SURF_METAL) { gi.DPrintf("Can't place landmine on metal\n"); return qfalse; } else if (trace.surfaceFlags & SURF_ROCK) { gi.DPrintf("Can't place landmine on rock\n"); return qfalse; } else if (trace.surfaceFlags & SURF_GRILL) { gi.DPrintf("Can't place landmine on grill\n"); return qfalse; } else if (trace.surfaceFlags & SURF_GLASS) { gi.DPrintf("Can't place landmine on glass\n"); return qfalse; } else if (trace.surfaceFlags & SURF_CARPET) { gi.DPrintf("Can't place landmine on carpet\n"); return qfalse; } if (!(trace.contents & CONTENTS_SOLID)) { gi.DPrintf("Can't place landmine on nonsolid things\n"); return qfalse; } return qtrue; } void PlaceLandmine(const Vector& origin, Entity *owner, const str& model, Weapon *weap) { SpawnArgs args; Listener *l; TriggerLandmine *trigger; if (!model.length()) { gi.DPrintf("PlaceLandmine : No model specified for PlaceLandmine"); return; } args.setArg("model", model.c_str()); args.setArg("setthread", "global/landmine.scr::steppedOn"); args.setArg("$defuseThread", "global/landmine.scr::defuse"); args.setArg("targetname", "landmine"); l = args.Spawn(); if (!l) { gi.DPrintf("PlaceLandmine model '%s' not found\n", model.c_str()); return; } if (!l->inheritsFrom(&TriggerLandmine::ClassInfo)) { gi.DPrintf("%s is not of class trigger_landmine\n", model.c_str()); return; } trigger = static_cast(l); trigger->droptofloor(256); trigger->ProcessInitCommands(); trigger->SetDamageable(qtrue); if (owner) { trigger->edict->r.ownerNum = owner->entnum; } trigger->setOrigin(origin); VectorCopy(trigger->origin, trigger->edict->s.origin2); if (owner->isSubclassOf(Player)) { if (g_gametype->integer >= GT_TEAM) { Player *p = static_cast(owner); trigger->SetTeam(p->GetTeam()); } else { trigger->SetTeam(0); } } trigger->NewAnim("idle"); if (g_gametype->integer == GT_SINGLE_PLAYER) { if (!owner) { weap = NULL; } else if (owner->IsDead() || owner == world) { weap = NULL; } if (weap) { weap->m_iNumShotsFired++; if (owner && owner->IsSubclassOfPlayer() && weap->IsSubclassOfTurretGun()) { Player *p = static_cast(owner); p->m_iNumShotsFired++; } } } } Projectile *ProjectileAttack( Vector start, Vector dir, Entity *owner, str projectileModel, float fraction, float real_speed, Weapon *weap ) { Event *ev; Projectile *proj = NULL; float newspeed, newlife; SpawnArgs args; Entity *obj; float dot = 0; if (!projectileModel.length()) { gi.DPrintf("ProjectileAttack : No model specified for ProjectileAttack"); return NULL; } args.setArg("model", projectileModel); // Added in 2.0 // Set the projectile targetname // so they can be managed from scripts args.setArg("targetname", "projectile"); obj = (Entity *)args.Spawn(); if (!obj) { gi.DPrintf("projectile model '%s' not found\n", projectileModel.c_str()); return NULL; } if (obj->isSubclassOf(Projectile)) { proj = (Projectile *)obj; } else { gi.DPrintf("%s is not of class projectile\n", projectileModel.c_str()); } if (!proj) { return NULL; } // Create a new projectile entity and set it off proj->setMoveType(MOVETYPE_BOUNCE); proj->ProcessInitCommands(); proj->SetOwner(owner); proj->edict->r.ownerNum = owner->entnum; proj->angles = dir.toAngles(); proj->charge_fraction = fraction; if (!real_speed) { if (proj->projFlags & P_CHARGE_SPEED) { newspeed = proj->minspeed + (proj->speed - proj->minspeed) * fraction; } else { newspeed = proj->speed; } } else { newspeed = real_speed; } if (proj->addownervelocity) { dot = DotProduct(owner->velocity, dir); if (dot < 0) { dot = 0; } } proj->velocity = dir * (newspeed + dot); proj->velocity += proj->addvelocity; proj->setAngles(proj->angles); proj->setSolidType(SOLID_BBOX); proj->edict->clipmask = MASK_PROJECTILE; proj->setSize(proj->mins, proj->maxs); proj->setOrigin(start); proj->origin.copyTo(proj->edict->s.origin2); if (proj->m_beam) { proj->m_beam->setOrigin(start); proj->m_beam->origin.copyTo(proj->m_beam->edict->s.origin2); } if (proj->dlight_radius) { G_SetConstantLight( &proj->edict->s.constantLight, &proj->dlight_color[0], &proj->dlight_color[1], &proj->dlight_color[2], &proj->dlight_radius ); } // Calc the life of the projectile if (proj->projFlags & P_CHARGE_LIFE) { if (g_gametype->integer != GT_SINGLE_PLAYER && proj->dmlife) { newlife = proj->dmlife * (1 - fraction); } else { newlife = proj->life * (1 - fraction); } if (newlife < proj->minlife) { newlife = proj->minlife; } } else { if (g_gametype->integer != GT_SINGLE_PLAYER && proj->dmlife) { newlife = proj->dmlife; } else { newlife = proj->life; } } // Remove the projectile after it's life expires ev = new Event(EV_Projectile_Explode); proj->PostEvent(ev, newlife); proj->NewAnim("idle"); // If can hit owner clear the owner of this projectile in a second if (proj->can_hit_owner) { proj->PostEvent(EV_Projectile_ClearOwner, 1); } if (owner) { if (owner->IsDead() || owner == world) { // clear the weapon as the owner died weap = NULL; } } else { // clear the weapon as there is no owner weap = NULL; } if (g_gametype->integer == GT_SINGLE_PLAYER) { if (weap) { weap->m_iNumShotsFired++; if (owner->IsSubclassOfPlayer() && weap->IsSubclassOfTurretGun()) { Player *p = (Player *)owner; p->m_iNumShotsFired++; } } } return proj; } void BulletAttack_Stat(Entity *owner, Entity *target, trace_t *trace, Weapon *weap) { Sentient *targetSen; if (!target->IsSubclassOfSentient() || !weap) { return; } targetSen = static_cast(target); targetSen->m_iLastHitTime = level.inttime; weap->m_iNumHits++; switch (trace->location) { case HITLOC_HEAD: case HITLOC_HELMET: case HITLOC_NECK: weap->m_iNumHeadShots++; break; case HITLOC_TORSO_UPPER: case HITLOC_TORSO_MID: case HITLOC_TORSO_LOWER: weap->m_iNumTorsoShots++; break; case HITLOC_PELVIS: weap->m_iNumGroinShots++; break; case HITLOC_R_ARM_UPPER: case HITLOC_R_ARM_LOWER: case HITLOC_R_HAND: weap->m_iNumRightArmShots++; break; case HITLOC_L_ARM_UPPER: case HITLOC_L_ARM_LOWER: case HITLOC_L_HAND: weap->m_iNumLeftArmShots++; break; case HITLOC_R_LEG_UPPER: case HITLOC_R_LEG_LOWER: case HITLOC_R_FOOT: weap->m_iNumRightLegShots++; break; case HITLOC_L_LEG_UPPER: case HITLOC_L_LEG_LOWER: case HITLOC_L_FOOT: weap->m_iNumLeftLegShots++; break; default: weap->m_iNumTorsoShots++; break; } if (owner && owner->IsSubclassOfPlayer() && weap->IsSubclassOfTurretGun()) { Player *p = static_cast(owner); p->m_iNumHits++; switch (trace->location) { case HITLOC_HEAD: case HITLOC_HELMET: case HITLOC_NECK: p->m_iNumHeadShots++; break; case HITLOC_TORSO_UPPER: case HITLOC_TORSO_MID: case HITLOC_TORSO_LOWER: p->m_iNumTorsoShots++; break; case HITLOC_PELVIS: p->m_iNumGroinShots++; break; case HITLOC_R_ARM_UPPER: case HITLOC_R_ARM_LOWER: case HITLOC_R_HAND: p->m_iNumRightArmShots++; break; case HITLOC_L_ARM_UPPER: case HITLOC_L_ARM_LOWER: case HITLOC_L_HAND: p->m_iNumLeftArmShots++; break; case HITLOC_R_LEG_UPPER: case HITLOC_R_LEG_LOWER: case HITLOC_R_FOOT: p->m_iNumRightLegShots++; break; case HITLOC_L_LEG_UPPER: case HITLOC_L_LEG_LOWER: case HITLOC_L_FOOT: p->m_iNumLeftLegShots++; break; default: p->m_iNumTorsoShots++; break; } } } float BulletAttack( Vector start, Vector vBarrel, Vector dir, Vector right, Vector up, float range, float damage, int bulletlarge, float knockback, int dflags, int meansofdeath, Vector spread, int count, Entity *owner, int iTracerFrequency, int *piTracerCount, float bulletthroughwood, float bulletthroughmetal, Weapon *weap, float tracerspeed ) { Vector vDir; Vector vTmpEnd; Vector vTraceStart; Vector vTraceEnd; int i; int iTravelDist; trace_t trace; Entity *ent; Entity *newowner; //Entity *tmpSkipEnt; float damage_total = 0; float original_value; qboolean bBulletDone; qboolean bThroughThing; int iContinueCount; vec3_t vEndArray[64]; int iTracerCount = 0; int iNumHit; int lastSurfaceFlags; float bulletdist; float newdamage; float throughThingFrac; float oldfrac; int bulletbits; lastSurfaceFlags = 0; iNumHit = 0; if (g_protocol >= protocol_e::PROTOCOL_MOHTA_MIN) { bulletbits = 2; } else { bulletlarge = damage >= 41.f; bulletbits = 1; } if (count > 63) { count = 63; } if (!owner || owner->IsDead() || owner == world) { weap = NULL; } for (i = 0; i < count; i++) { trace_t tracethrough; vTraceEnd = start + (dir * range) + (right * grandom() * spread.x) + (up * grandom() * spread.y); vDir = vTraceEnd - start; VectorNormalizeFast(vDir); iContinueCount = 0; iTravelDist = 0; bBulletDone = qfalse; bThroughThing = qfalse; newowner = owner; newdamage = damage; while (!bBulletDone && iTravelDist < MAX_TRAVEL_DIST) { iTravelDist += MAX_TRAVEL_DIST; vTraceStart = start; vTraceEnd = start + vDir * iTravelDist; memset(&trace, 0, sizeof(trace_t)); oldfrac = -1; while (trace.fraction < 1.0f) { Vector vDeltaTrace; trace = G_Trace( vTraceStart, vec_zero, vec_zero, vTraceEnd, newowner, MASK_SHOT_TRIG, false, "BulletAttack", true ); vTmpEnd = trace.endpos; if (bThroughThing) { bThroughThing = qfalse; tracethrough = G_Trace( vTmpEnd, vec_zero, vec_zero, vTraceStart + vDir * -4, newowner, MASK_SHOT, qfalse, "BulletAttack2", qtrue ); if (!(tracethrough.surfaceFlags & (SURF_FOLIAGE | SURF_GLASS | SURF_PUDDLE | SURF_PAPER)) && (!(tracethrough.surfaceFlags & SURF_WOOD) || !bulletthroughwood) && (!(tracethrough.surfaceFlags & (SURF_GRILL | SURF_METAL)) || !bulletthroughmetal)) { vTmpEnd = vTraceStart + vDir * -4; trace.fraction = 1.f; bBulletDone = qtrue; if (g_showbullettrace->integer) { bThroughThing = qtrue; } break; } if (lastSurfaceFlags & SURF_WOOD) { if (tracethrough.surfaceFlags & SURF_WOOD) { throughThingFrac = 1.f / bulletthroughwood; } else { throughThingFrac = 2.f / (bulletthroughwood + bulletthroughmetal); } } else { if (tracethrough.surfaceFlags & SURF_WOOD) { throughThingFrac = 2.f / (bulletthroughwood + bulletthroughmetal); } else { throughThingFrac = 1.f / bulletthroughmetal; } } bulletdist = (tracethrough.endpos - vTraceStart).length() + 4.f; if (g_showbullettrace->integer) { gi.Printf("Bullet damage: %.2f : %.2f -> ", bulletdist, damage); } newdamage -= damage * bulletdist * throughThingFrac; if (newdamage < 1.f) { vTmpEnd = vTraceStart + vDir * -4; trace.fraction = 1.f; bBulletDone = qtrue; if (g_showbullettrace->integer) { bThroughThing = qtrue; } break; } if (g_showbullettrace->integer) { G_DebugLine( tracethrough.endpos + Vector(8, 0, 0), tracethrough.endpos - Vector(8, 0, 0), 0.5f, 0.5f, 1.f, 1.f ); G_DebugLine( tracethrough.endpos + Vector(0, 8, 0), tracethrough.endpos - Vector(0, 8, 0), 0.5f, 0.5f, 1.f, 1.f ); G_DebugLine( tracethrough.endpos + Vector(0, 0, 8), tracethrough.endpos - Vector(0, 0, 8), 0.5f, 0.5f, 1.f, 1.f ); } } if (trace.ent) { ent = trace.ent->entity; } else { ent = NULL; } if (ent && ent != world && ent != owner) { if (ent->takedamage) { if (g_gametype->integer == GT_SINGLE_PLAYER && !iNumHit) { BulletAttack_Stat(owner, ent, &trace, weap); } iNumHit++; // Get the original value of the victims health or water original_value = ent->health; ent->Damage( world, owner, newdamage, trace.endpos, dir, trace.plane.normal, knockback, dflags, meansofdeath, trace.location ); // Get the new value of the victims health or water damage_total += original_value - ent->health; } if (ent->edict->solid == SOLID_BBOX && !(trace.contents & CONTENTS_CLAYPIDGEON)) { if (trace.surfaceFlags & MASK_SURF_TYPE) { gi.SetBroadcastVisible(vTmpEnd, NULL); gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_6)); gi.MSG_WriteCoord(vTmpEnd[0]); gi.MSG_WriteCoord(vTmpEnd[1]); gi.MSG_WriteCoord(vTmpEnd[2]); gi.MSG_WriteDir(trace.plane.normal); gi.MSG_WriteBits(bulletlarge, bulletbits); gi.MSG_EndCGM(); } else if (trace.location >= 0 && ent->IsSubclassOfSentient()) { gi.SetBroadcastVisible(vTmpEnd, NULL); gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_8)); gi.MSG_WriteCoord(vTmpEnd[0]); gi.MSG_WriteCoord(vTmpEnd[1]); gi.MSG_WriteCoord(vTmpEnd[2]); gi.MSG_WriteDir(trace.plane.normal); gi.MSG_WriteBits(bulletlarge, bulletbits); gi.MSG_EndCGM(); } else if (ent->edict->r.contents & CONTENTS_SOLID) { gi.SetBroadcastVisible(vTmpEnd, NULL); gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_7)); gi.MSG_WriteCoord(vTmpEnd[0]); gi.MSG_WriteCoord(vTmpEnd[1]); gi.MSG_WriteCoord(vTmpEnd[2]); gi.MSG_WriteDir(trace.plane.normal); gi.MSG_WriteBits(bulletlarge, bulletbits); gi.MSG_EndCGM(); } } else if (ent->edict->solid == SOLID_BSP && !(trace.contents & CONTENTS_CLAYPIDGEON)) { gi.SetBroadcastVisible(vTmpEnd, NULL); gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_6)); gi.MSG_WriteCoord(vTmpEnd[0]); gi.MSG_WriteCoord(vTmpEnd[1]); gi.MSG_WriteCoord(vTmpEnd[2]); gi.MSG_WriteDir(trace.plane.normal); gi.MSG_WriteBits(bulletlarge, bulletbits); gi.MSG_EndCGM(); } } if (trace.fraction < 1.0f) { if (trace.surfaceFlags & (SURF_FOLIAGE | SURF_GLASS | SURF_PUDDLE | SURF_PAPER) || trace.contents & (CONTENTS_CLAYPIDGEON | CONTENTS_WATER) || (g_protocol < protocol_e::PROTOCOL_MOHTA && trace.startsolid) || (bulletlarge && trace.ent && (g_protocol < protocol_e::PROTOCOL_MOHTA || (trace.ent->r.contents & CONTENTS_BBOX)) && !trace.ent->r.bmodel && trace.ent->entity->takedamage) || ((trace.surfaceFlags & SURF_WOOD) && bulletthroughwood) || ((trace.surfaceFlags & (SURF_GRILL | SURF_METAL)) && bulletthroughmetal && iContinueCount < 5)) { if (((trace.surfaceFlags & SURF_WOOD) && bulletthroughwood) || ((trace.surfaceFlags & (SURF_GRILL | SURF_METAL)) && bulletthroughmetal)) { if (trace.contents & CONTENTS_FENCE) { float damageMultiplier; if (lastSurfaceFlags & SURF_WOOD) { damageMultiplier = 1.f / bulletthroughwood; } else { damageMultiplier = 1.f / bulletthroughmetal; } newdamage -= damageMultiplier * 2 * damage; if (newdamage < 0) { trace.fraction = 1; bBulletDone = qtrue; if (g_showbullettrace->integer) { bThroughThing = qtrue; VectorScale(vDir, 2, tracethrough.endpos); VectorAdd(tracethrough.endpos, vTmpEnd, tracethrough.endpos); } } else { trace.fraction = 1; bBulletDone = qtrue; if (g_showbullettrace->integer) { bThroughThing = qtrue; VectorScale(vDir, 2, tracethrough.endpos); VectorAdd(tracethrough.endpos, vTmpEnd, tracethrough.endpos); } } } else { bThroughThing = qtrue; lastSurfaceFlags = trace.surfaceFlags; } } if (!bBulletDone) { vTraceStart = vTmpEnd + vDir * 4; if (trace.ent) { newowner = trace.ent->entity; } else { newowner = NULL; } if (g_showbullettrace->integer) { G_DebugLine(vTmpEnd + Vector(8, 0, 0), vTmpEnd - Vector(8, 0, 0), 1, 0.5f, 0.5f, 1.f); G_DebugLine(vTmpEnd + Vector(0, 8, 0), vTmpEnd - Vector(0, 8, 0), 1, 0.5f, 0.5f, 1.f); G_DebugLine(vTmpEnd + Vector(0, 0, 8), vTmpEnd - Vector(0, 0, 8), 1, 0.5f, 0.5f, 1.f); } iContinueCount++; } } else { trace.fraction = 1; bBulletDone = qtrue; } } if (oldfrac != trace.fraction) { oldfrac = trace.fraction; } else { trace.fraction = 1; } vDeltaTrace = vTmpEnd - vTraceStart; if (!bBulletDone && DotProduct(vDeltaTrace, vDir) < 0) { // Fixed in OPM // This can happen in rare circumstances if the trace is out of the world limit break; } } } if (bBulletDone && g_showbullettrace->integer && bThroughThing) { G_DebugLine( tracethrough.endpos + Vector(8, 0, 0), tracethrough.endpos - Vector(8, 0, 0), 0.25f, 0.25f, 0.5f, 1.f ); G_DebugLine( tracethrough.endpos + Vector(0, 8, 0), tracethrough.endpos - Vector(0, 8, 0), 0.25f, 0.25f, 0.5f, 1.f ); G_DebugLine( tracethrough.endpos + Vector(0, 0, 8), tracethrough.endpos - Vector(0, 0, 8), 0.25f, 0.25f, 0.5f, 1.f ); } VectorCopy(vTmpEnd, vEndArray[i]); if (iTracerFrequency && piTracerCount) { (*piTracerCount)++; if (*piTracerCount == iTracerFrequency) { iTracerCount++; *piTracerCount = 0; } } // Draw a debug trace line to show bullet fire if (g_showbullettrace->integer) { G_DebugLine(start, vTmpEnd, 1, 1, 1, 1); G_DebugLine(vTmpEnd + Vector(8, 0, 0), vTmpEnd - Vector(8, 0, 0), 0.5f, 0.25f, 0.25f, 1.f); G_DebugLine(vTmpEnd + Vector(0, 8, 0), vTmpEnd - Vector(0, 8, 0), 0.5f, 0.25f, 0.25f, 1.f); G_DebugLine(vTmpEnd + Vector(0, 0, 8), vTmpEnd - Vector(0, 0, 8), 0.5f, 0.25f, 0.25f, 1.f); G_DebugLine(vTmpEnd, vTraceEnd, 0.4f, 0.4f, 0.4f, 1.f); } } if (g_gametype->integer == GT_SINGLE_PLAYER && weap) { weap->m_iNumShotsFired++; if (owner && owner->IsSubclassOfPlayer() && weap->IsSubclassOfTurretGun()) { Player *p = (Player *)owner; p->m_iNumShotsFired++; } } gi.SetBroadcastVisible(start, vEndArray[0]); if (count == 1) { if (iTracerCount) { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_1)); gi.MSG_WriteCoord(vBarrel[0]); gi.MSG_WriteCoord(vBarrel[1]); gi.MSG_WriteCoord(vBarrel[2]); } else { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_2)); } gi.MSG_WriteCoord(start[0]); gi.MSG_WriteCoord(start[1]); gi.MSG_WriteCoord(start[2]); gi.MSG_WriteCoord(vEndArray[0][0]); gi.MSG_WriteCoord(vEndArray[0][1]); gi.MSG_WriteCoord(vEndArray[0][2]); gi.MSG_WriteBits(bulletlarge, bulletbits); if (g_protocol >= protocol_e::PROTOCOL_MOHTA_MIN) { if (tracerspeed == 1.f) { gi.MSG_WriteBits(0, 1); } else { int speed; speed = tracerspeed * (1 << 9); gi.MSG_WriteBits(1, 1); gi.MSG_WriteBits(Q_clamp(speed, 1, 1023), 10); } } } else { if (iTracerCount) { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_3)); gi.MSG_WriteCoord(vBarrel[0]); gi.MSG_WriteCoord(vBarrel[1]); gi.MSG_WriteCoord(vBarrel[2]); if (iTracerCount > 63) { iTracerCount = 63; } gi.MSG_WriteBits(Q_min(iTracerCount, 63), 6); } else { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_4)); } gi.MSG_WriteCoord(start[0]); gi.MSG_WriteCoord(start[1]); gi.MSG_WriteCoord(start[2]); gi.MSG_WriteBits(bulletlarge, bulletbits); if (g_protocol >= protocol_e::PROTOCOL_MOHTA_MIN) { if (tracerspeed == 1.f) { gi.MSG_WriteBits(0, 1); } else { int speed; speed = tracerspeed * (1 << 9); gi.MSG_WriteBits(1, 1); gi.MSG_WriteBits(Q_clamp(speed, 1, 1023), 10); } } gi.MSG_WriteBits(count, 6); for (int i = 0; i < count; i++) { gi.MSG_WriteCoord(vEndArray[i][0]); gi.MSG_WriteCoord(vEndArray[i][1]); gi.MSG_WriteCoord(vEndArray[i][2]); } } gi.MSG_EndCGM(); if (damage_total > 0) { return damage_total; } else { return 0; } } void FakeBulletAttack( Vector start, Vector vBarrel, Vector dir, Vector right, Vector up, float range, float damage, int large, Vector spread, int count, Entity *owner, int iTracerFrequency, int *piTracerCount, float tracerspeed ) { Vector vDir; Vector vTraceEnd; int i; int bulletbits; if (g_protocol >= protocol_e::PROTOCOL_MOHTA_MIN) { bulletbits = 2; } else { large = damage >= 41.f; bulletbits = 1; } for (i = 0; i < count; i++) { vTraceEnd = start + (dir * range) + (right * grandom() * spread.x) + (up * grandom() * spread.y); vDir = vTraceEnd - start; VectorNormalize(vDir); vTraceEnd = start + vDir * 9216.0f; gi.SetBroadcastVisible(start, vTraceEnd); if (iTracerFrequency && piTracerCount) { (*piTracerCount)++; if (*piTracerCount == iTracerFrequency) { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_NO_BARREL_1)); gi.MSG_WriteCoord(vBarrel[0]); gi.MSG_WriteCoord(vBarrel[1]); gi.MSG_WriteCoord(vBarrel[2]); *piTracerCount = 0; } else { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_NO_BARREL_2)); } } else { gi.MSG_StartCGM(BG_MapCGMToProtocol(g_protocol, CGM_BULLET_NO_BARREL_2)); } gi.MSG_WriteCoord(start[0]); gi.MSG_WriteCoord(start[1]); gi.MSG_WriteCoord(start[2]); gi.MSG_WriteCoord(vTraceEnd[0]); gi.MSG_WriteCoord(vTraceEnd[1]); gi.MSG_WriteCoord(vTraceEnd[2]); gi.MSG_WriteBits(large, bulletbits); if (g_protocol >= protocol_e::PROTOCOL_MOHTA_MIN) { if (tracerspeed == 1.f) { gi.MSG_WriteBits(0, 1); } else { int speed; speed = tracerspeed * (1 << 9); gi.MSG_WriteBits(1, 1); gi.MSG_WriteBits(Q_clamp(speed, 1, 1023), 10); } } gi.MSG_EndCGM(); } } void ClickItemAttack(Vector vStart, Vector vForward, float fRange, Entity *pOwner) { Event *ev; Vector vEnd; trace_t trace; vEnd = vStart + vForward * fRange; trace = G_Trace(vStart, vec_zero, vec_zero, vEnd, pOwner, MASK_CLICKITEM, qfalse, "ClickItemAttack"); if (g_showbullettrace->integer) { //G_DebugLine(vStart, vEnd, 1, 1, 1, 1); // Added in OPM // White line between start and end trace position G_DebugLine(vStart, trace.endpos, 1, 1, 1, 1); G_DebugLine(trace.endpos, vEnd, 1, 0, 0, 1); } if (trace.entityNum == ENTITYNUM_WORLD || !trace.ent || !trace.ent->entity || !trace.ent->entity->isSubclassOf(TriggerClickItem)) { ScriptThreadLabel failThread; // Try to execute a fail thread if (failThread.TrySet("clickitem_fail")) { failThread.Execute(); } return; } ev = new Event(EV_Activate); ev->AddEntity(pOwner); trace.ent->entity->PostEvent(ev, 0); } Projectile *HeavyAttack(Vector start, Vector dir, str projectileModel, float real_speed, Entity *owner, Weapon *weap) { Event *ev; Projectile *proj = NULL; float newspeed, newlife; SpawnArgs args; Entity *obj; float dot = 0; if (!projectileModel.length()) { gi.DPrintf("ProjectileAttack : No model specified for ProjectileAttack"); return NULL; } args.setArg("model", projectileModel); obj = (Entity *)args.Spawn(); if (!obj) { gi.DPrintf("projectile model '%s' not found\n", projectileModel.c_str()); return NULL; } if (obj->IsSubclassOfProjectile()) { proj = (Projectile *)obj; } else { gi.DPrintf("%s is not of class projectile\n", projectileModel.c_str()); } if (!proj) { return NULL; } // Create a new projectile entity and set it off proj->setModel(projectileModel); proj->setMoveType(MOVETYPE_BOUNCE); proj->ProcessInitCommands(); proj->SetOwner(owner); proj->edict->r.ownerNum = owner->entnum; proj->angles = dir.toAngles(); proj->charge_fraction = 1.0f; newspeed = real_speed; if (real_speed == 0.0f) { newspeed = proj->speed; } if (proj->addownervelocity) { dot = DotProduct(owner->velocity, dir); if (dot < 0) { dot = 0; } } proj->velocity = dir * (newspeed + dot); proj->velocity += proj->addvelocity; proj->setAngles(proj->angles); proj->setSolidType(SOLID_BBOX); proj->edict->clipmask = MASK_PROJECTILE; proj->setSize(proj->mins, proj->maxs); proj->setOrigin(start); proj->origin.copyTo(proj->edict->s.origin2); if (proj->m_beam) { proj->m_beam->setOrigin(start); proj->m_beam->origin.copyTo(proj->m_beam->edict->s.origin2); } if (proj->dlight_radius) { G_SetConstantLight( &proj->edict->s.constantLight, &proj->dlight_color[0], &proj->dlight_color[1], &proj->dlight_color[2], &proj->dlight_radius ); } // Calc the life of the projectile if (proj->projFlags & P_CHARGE_LIFE) { if (g_gametype->integer && proj->dmlife) { newlife = proj->dmlife; } else { newlife = proj->life; } if (newlife < proj->minlife) { newlife = proj->minlife; } } else { if (g_gametype->integer && proj->dmlife) { newlife = proj->dmlife; } else { newlife = proj->life; } } // Remove the projectile after it's life expires ev = new Event(EV_Projectile_Explode); proj->PostEvent(ev, newlife); proj->NewAnim("idle"); if (!g_gametype->integer) { if (weap) { weap->m_iNumShotsFired++; if (owner->IsSubclassOfPlayer() && weap->IsSubclassOfTurretGun()) { Player *p = (Player *)owner; p->m_iNumShotsFired++; } } } return proj; } void ExplosionAttack( Vector pos, Entity *owner, str explosionModel, Vector dir, Entity *ignore, float scale, Weapon *weap, bool hurtOwnerOnly ) { Explosion *explosion; Event *ev; if (!owner) { owner = world; } if (owner->IsDead() || owner == world) { weap = NULL; } if (explosionModel.length() && gi.modeltiki(CanonicalTikiName(explosionModel))) { SpawnArgs sp; ClassDef *c; sp.setArg("model", explosionModel); c = sp.getClassDef(); if (c && c != Explosion::classinfostatic() && checkInheritance(Entity::classinfostatic(), c)) { Entity *newent = static_cast(c->newInstance()); newent->setModel(explosionModel); newent->setSolidType(SOLID_NOT); newent->setOrigin(pos); newent->setAngles(dir.toAngles()); newent->ProcessInitCommands(); if (c == SmokeGrenade::classinfostatic()) { // // try to set the owner of the smoke grenade // SmokeGrenade *smoke = static_cast(newent); if (owner && owner->IsSubclassOfSentient()) { smoke->setOwner(static_cast(owner)); } } } else { explosion = new Explosion; // Create a new explosion entity and set it off explosion->setModel(explosionModel); explosion->setSolidType(SOLID_NOT); // Process the INIT commands right away explosion->ProcessInitCommands(); explosion->SetOwner(owner); explosion->edict->r.ownerNum = owner->entnum; explosion->angles = dir.toAngles(); explosion->velocity = dir * explosion->speed; explosion->edict->s.scale = scale; explosion->setAngles(explosion->angles); explosion->setMoveType(MOVETYPE_FLYMISSILE); explosion->edict->clipmask = MASK_PROJECTILE; explosion->setSize(explosion->mins, explosion->maxs); explosion->setOrigin(pos); explosion->origin.copyTo(explosion->edict->s.origin2); explosion->hurtOwnerOnly = hurtOwnerOnly; if (explosion->dlight_radius) { G_SetConstantLight( &explosion->edict->s.constantLight, &explosion->dlight_color[0], &explosion->dlight_color[1], &explosion->dlight_color[2], &explosion->dlight_radius ); } explosion->BroadcastAIEvent(AI_EVENT_WEAPON_FIRE); explosion->NewAnim("idle"); RadiusDamage( explosion->origin, explosion, owner, explosion->radius_damage * scale, ignore, explosion->meansofdeath, explosion->radius * scale, explosion->knockback, explosion->constant_damage, weap, explosion->hurtOwnerOnly ); if (explosion->flash_radius) { FlashPlayers( explosion->origin, explosion->flash_r, explosion->flash_g, explosion->flash_b, explosion->flash_a, explosion->flash_radius * scale, explosion->flash_time, explosion->flash_type ); } if (explosion->damage_every_frame) { explosion->PostEvent(EV_Explosion_DamageAgain, FRAMETIME); } // Remove explosion after the life has expired if (explosion->life || (g_gametype->integer && explosion->dmlife)) { ev = new Event(EV_Remove); if (g_gametype->integer && explosion->dmlife) { explosion->PostEvent(ev, explosion->dmlife); } else { explosion->PostEvent(ev, explosion->life); } } } } } static float radiusdamage_origin[3]; static int radiusdamage_compare(const void *elem1, const void *elem2) { Entity *e1, *e2; float delta[3]; float d1, d2; e1 = *(Entity **)elem1; e2 = *(Entity **)elem2; VectorSubtract(radiusdamage_origin, e1->origin, delta); d1 = VectorLengthSquared(delta); VectorSubtract(radiusdamage_origin, e2->origin, delta); d2 = VectorLengthSquared(delta); if (d2 <= d1) { return d1 > d2; } else { return -1; } } void RadiusDamage( Vector origin, Entity *inflictor, Entity *attacker, float damage, Entity *ignore, int mod, float radius, float knockback, qboolean constant_damage, Weapon *weap, bool hurtOwnerOnly ) { float points; Entity *ent; Vector org; Vector dir; float dist; int i; Container ents; if (g_showdamage->integer) { Com_Printf("radiusdamage"); Com_Printf("{\n"); Com_Printf("origin: %f %f %f\n", origin[0], origin[1], origin[2]); if (inflictor) { Com_Printf("inflictor: (entnum %d, radnum %d)\n", inflictor->entnum, inflictor->radnum); } if (attacker) { Com_Printf("attacker: (entnum %d, radnum %d)\n", attacker->entnum, attacker->radnum); } Com_Printf("damage: %f\n", damage); if (ignore) { Com_Printf("ignore: (entnum %d, radnum %d)\n", ignore->entnum, ignore->radnum); } Com_Printf("mod: %d\n", mod); Com_Printf("radius: %f\n", radius); Com_Printf("knockback: %f\n", knockback); Com_Printf("constant_damage: %d\n", constant_damage); if (weap) { Com_Printf("weapon %s\n", weap->getName().c_str()); } Com_Printf("hurtOwnerOnly: %d\n", hurtOwnerOnly); Com_Printf("}\n"); } ent = findradius(NULL, origin, radius); while (ent) { // Add ents that has contents if (ent->getContents()) { ents.AddObject(ent); } ent = findradius(ent, origin, radius); } // Sort by the nearest to the fartest entity if (ents.NumObjects()) { VectorCopy(origin, radiusdamage_origin); ents.Sort(radiusdamage_compare); } for (i = 1; i <= ents.NumObjects(); i++) { ent = ents.ObjectAt(i); if (ent == ignore || !(ent->takedamage) || (hurtOwnerOnly && ent != attacker)) { continue; } // Add this in for deathmatch maybe if (ent->getContents() == CONTENTS_CLAYPIDGEON || G_SightTrace( origin, vec_zero, vec_zero, ent->centroid, inflictor, ent, MASK_EXPLOSION, false, "RadiusDamage" )) { if (constant_damage) { points = damage; } else { float ent_rad; ent_rad = fabs(ent->maxs[0] - ent->mins[0]); if (ent_rad < fabs(ent->maxs[1] - ent->mins[1])) { ent_rad = fabs(ent->maxs[1] - ent->mins[1]); } if (ent_rad < fabs(ent->maxs[2] - ent->mins[2])) { ent_rad = fabs(ent->maxs[2] - ent->mins[2]); } org = ent->centroid; dir = org - origin; dist = dir.length() - ent_rad; if (dist < 0.0f) { dist = 0.0f; } points = damage - damage * (dist / radius); knockback -= knockback * (dist / radius); if (points < 0) { points = 0; } if (knockback < 0) { knockback = 0; } } // reduce the damage a little for self-damage if (ent == attacker) { points *= 0.9f; } if (points > 0) { ent->Damage(inflictor, attacker, points, org, dir, vec_zero, knockback, DAMAGE_RADIUS, mod); if (g_gametype->integer == GT_SINGLE_PLAYER && weap) { if (ent->IsSubclassOfPlayer() || ent->IsSubclassOfVehicle() || ent->IsSubclassOfVehicleTank() || ent->isSubclassOf(VehicleCollisionEntity)) { weap->m_iNumHits++; weap->m_iNumTorsoShots++; if (attacker && attacker->IsSubclassOfPlayer() && weap->IsSubclassOfTurretGun()) { Player *player = static_cast(attacker); player->m_iNumHits++; player->m_iNumTorsoShots++; } } } } } } if (mod == MOD_EXPLOSION) { // Create an earthquake new ViewJitter( origin, radius + 128.0f, 0.05f, Vector(damage * 0.05f, damage * 0.05f, damage * 0.06f), 0, vec_zero, 0 ); } } void FlashPlayers(Vector org, float r, float g, float b, float a, float radius, float time, int type) { trace_t trace; Vector delta; float length; Player *player; gentity_t *ed; int i; Entity *ent; float newa = 1; for (i = 0; i < game.maxclients; i++) { ed = &g_entities[i]; if (!ed->inuse || !ed->entity) { continue; } ent = ed->entity; if (!ent->IsSubclassOfPlayer()) { continue; } player = (Player *)ent; if (!player->WithinDistance(org, radius)) { continue; } trace = G_Trace(org, vec_zero, vec_zero, player->origin, player, MASK_OPAQUE, qfalse, "FlashPlayers"); if (trace.fraction != 1.0) { continue; } delta = org - trace.endpos; length = delta.length(); // If alpha is specified, then modify it by the amount of distance away from the flash the player is if (a != -1) { newa = a * (1 - length / radius); } level.m_fade_alpha = newa; level.m_fade_color[0] = r; level.m_fade_color[1] = g; level.m_fade_color[2] = b; level.m_fade_time = time; level.m_fade_time_start = time; if (type == 1) { level.m_fade_style = additive; } else { level.m_fade_style = alphablend; } // gi.SendServerCommand( NULL, va( "fadein %0.2f %0.2f %0.2f %0.2f %i",time*1000,r*newa,g*newa,b*newa,type ) ); } } const char *G_LocationNumToDispString(int iLocation) { switch (iLocation) { case -2: case -1: return ""; case 0: return "head"; case 1: return "helmet"; case 2: return "neck"; case 3: return "upper torso"; case 4: return "middle torso"; case 5: return "lower torso"; case 6: return "pelvis"; case 7: return "upper right arm"; case 8: return "upper left arm"; case 9: return "upper right leg"; case 10: return "upper left leg"; case 11: return "lower right arm"; case 12: return "lower left arm"; case 13: return "lower right leg"; case 14: return "lower left leg"; case 15: return "right hand"; case 16: return "left hand"; case 17: return "right foot"; case 18: return "left foot"; default: return ""; } }