예제 #1
0
void G_ExplodeMissile( gentity_t *ent )
{
	vec3_t dir;
	vec3_t origin;
	const missileAttributes_t *ma = BG_Missile( ent->s.modelindex );

	BG_EvaluateTrajectory( &ent->s.pos, level.time, origin );
	SnapVector( origin );
	G_SetOrigin( ent, origin );

	// we don't have a valid direction, so just point straight up
	dir[ 0 ] = dir[ 1 ] = 0;
	dir[ 2 ] = 1;

	// turn the missile into an event carrier
	ent->s.eType = ET_INVISIBLE;
	ent->freeAfterEvent = true;
	G_AddEvent( ent, EV_MISSILE_HIT_ENVIRONMENT, DirToByte( dir ) );

	// splash damage
	if ( ent->splashDamage )
	{
		G_RadiusDamage( ent->r.currentOrigin, ent->parent,
		                ent->splashDamage * MissileTimeSplashDmgMod( ent ),
		                ent->splashRadius, ent, ( ma->doKnockback ? DAMAGE_KNOCKBACK : 0 ),
		                ent->splashMethodOfDeath );
	}

	trap_LinkEntity( ent );
}
예제 #2
0
static void RocketThink( gentity_t *self )
{
	vec3_t currentDir, targetDir, newDir, rotAxis;
	float  rotAngle;

	if ( level.time > self->timestamp )
	{
		self->think     = G_ExplodeMissile;
		self->nextthink = level.time;

		return;
	}

	self->nextthink = level.time + ROCKET_TURN_PERIOD;

	// Calculate current and target direction.
	VectorNormalize2( self->s.pos.trDelta, currentDir );
	VectorSubtract( self->target->r.currentOrigin, self->r.currentOrigin, targetDir );
	VectorNormalize( targetDir );

	// Don't turn anymore after the target was passed.
	if ( DotProduct( currentDir, targetDir ) < 0 )
	{
		return;
	}

	// Calculate new direction. Use a fixed turning angle.
	CrossProduct( currentDir, targetDir, rotAxis );
	rotAngle = RAD2DEG( acos( DotProduct( currentDir, targetDir ) ) );
	RotatePointAroundVector( newDir, rotAxis, currentDir,
	                         Math::Clamp( rotAngle, -ROCKET_TURN_ANGLE, ROCKET_TURN_ANGLE ) );

	// Check if new direction is safe. Turn anyway if old direction is unsafe, too.
	if ( !G_RocketpodSafeShot( ENTITYNUM_NONE, self->r.currentOrigin, newDir ) &&
	     G_RocketpodSafeShot( ENTITYNUM_NONE, self->r.currentOrigin, currentDir ) )
	{
		return;
	}

	// Update trajectory.
	VectorScale( newDir, BG_Missile( self->s.modelindex )->speed, self->s.pos.trDelta );
	SnapVector( self->s.pos.trDelta );
	VectorCopy( self->r.currentOrigin, self->s.pos.trBase ); // TODO: Snap this, too?
	self->s.pos.trTime = level.time;
}
예제 #3
0
bool G_RocketpodSafeShot( int passEntityNum, vec3_t origin, vec3_t dir )
{
	trace_t tr;
	vec3_t mins, maxs, end;
	float  size;
	const missileAttributes_t *attr = BG_Missile( MIS_ROCKET );

	size = attr->size;

	VectorSet( mins, -size, -size, -size);
	VectorSet( maxs, size, size, size );
	VectorMA( origin, 8192, dir, end );

	trap_Trace( &tr, origin, mins, maxs, end, passEntityNum, MASK_SHOT, 0 );

	return !G_RadiusDamage( tr.endpos, nullptr, attr->splashDamage, attr->splashRadius, nullptr,
	                        0, MOD_ROCKETPOD, TEAM_HUMANS );
}
예제 #4
0
// TODO: Use a proper trajectory trace, once available, to ensure friendly buildables are never hit.
bool SpikerComponent::SafeToShoot(Vec3 direction) {
	const missileAttributes_t* ma = BG_Missile(MIS_SPIKER);
	float missileSize = (float)ma->size;
	trace_t trace;
	vec3_t mins, maxs;
	Vec3 end = Vec3::Load(entity.oldEnt->s.origin) + (SPIKE_RANGE * direction);

	// Test once with normal and once with inflated missile bounding box.
	for (float traceSize : {missileSize, missileSize * SAFETY_TRACE_INFLATION}) {
		mins[0] = mins[1] = mins[2] = -traceSize;
		maxs[0] = maxs[1] = maxs[2] =  traceSize;
		trap_Trace(&trace, entity.oldEnt->s.origin, mins, maxs, end.Data(), entity.oldEnt->s.number,
			ma->clipmask, 0);
		gentity_t* hit = &g_entities[trace.entityNum];

		if (hit && G_OnSameTeam(entity.oldEnt, hit)) {
			return false;
		}
	}

	return true;
}
예제 #5
0
gentity_t *G_SpawnMissile( missile_t missile, gentity_t *parent, vec3_t start, vec3_t dir,
                           gentity_t *target, void ( *think )( gentity_t *self ), int nextthink )
{
	gentity_t                 *m;
	const missileAttributes_t *ma;
	vec3_t                    velocity;

	if ( !parent )
	{
		return nullptr;
	}

	ma = BG_Missile( missile );

	m = G_NewEntity();

	// generic
	m->s.eType             = ET_MISSILE;
	m->s.modelindex        = missile;
	m->r.ownerNum          = parent->s.number;
	m->parent              = parent;
	m->target              = target;
	m->think               = think;
	m->nextthink           = nextthink;

	// from attribute config file
	m->s.weapon            = ma->number;
	m->classname           = ma->name;
	m->pointAgainstWorld   = ma->pointAgainstWorld;
	m->damage              = ma->damage;
	m->methodOfDeath       = ma->meansOfDeath;
	m->splashDamage        = ma->splashDamage;
	m->splashRadius        = ma->splashRadius;
	m->splashMethodOfDeath = ma->splashMeansOfDeath;
	m->clipmask            = ma->clipmask;
	m->r.mins[ 0 ]         =
	m->r.mins[ 1 ]         =
	m->r.mins[ 2 ]         = -ma->size;
	m->r.maxs[ 0 ]         =
	m->r.maxs[ 1 ]         =
	m->r.maxs[ 2 ]         = ma->size;
	m->s.eFlags            = ma->flags;

	// not yet implemented / deprecated
	m->flightSplashDamage  = 0;
	m->flightSplashRadius  = 0;

	// trajectory
	{
		// set trajectory type
		m->s.pos.trType = ma->trajectoryType;

		// move a bit on the first frame
		m->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME;

		// set starting point
		VectorCopy( start, m->s.pos.trBase );
		VectorCopy( start, m->r.currentOrigin );

		// set speed
		VectorScale( dir, ma->speed, velocity );

		// add lag
		if ( ma->lag && parent->client )
		{
			VectorMA( velocity, ma->lag, parent->client->ps.velocity, velocity );
		}

		// copy velocity
		VectorCopy( velocity, m->s.pos.trDelta );

		// save net bandwidth
		SnapVector( m->s.pos.trDelta );
	}

	return m;
}
예제 #6
0
static void MissileImpact( gentity_t *ent, trace_t *trace )
{
	int       dirAsByte, impactFlags;
	const missileAttributes_t *ma = BG_Missile( ent->s.modelindex );
	gentity_t *hitEnt   = &g_entities[ trace->entityNum ];
	gentity_t *attacker = &g_entities[ ent->r.ownerNum ];

	// Returns whether damage and hit effects should be done and played.
	std::function<int(gentity_t*, trace_t*, gentity_t*)> impactFunc;

	// Check for bounce.
	if ( ent->s.eFlags & ( EF_BOUNCE | EF_BOUNCE_HALF ) &&
	     !HasComponents<HealthComponent>(*hitEnt->entity) )
	{
		BounceMissile( ent, trace );

		if ( !( ent->s.eFlags & EF_NO_BOUNCE_SOUND ) )
		{
			G_AddEvent( ent, EV_GRENADE_BOUNCE, 0 );
		}

		return;
	}

	// Call missile specific impact functions.
	switch( ent->s.modelindex )
	{
		case MIS_GRENADE:      impactFunc = ImpactGrenade;     break;
		case MIS_FIREBOMB:     impactFunc = ImpactGrenade;     break;
		case MIS_FLAMER:       impactFunc = ImpactFlamer;      break;
		case MIS_FIREBOMB_SUB: impactFunc = ImpactFirebombSub; break;
		case MIS_LOCKBLOB:     impactFunc = ImpactLockblock;   break;
		case MIS_SLOWBLOB:     impactFunc = ImpactSlowblob;    break;
		case MIS_HIVE:         impactFunc = ImpactHive;        break;
		default:               impactFunc = DefaultImpactFunc; break;
	}

	impactFlags = impactFunc( ent, trace, hitEnt );

	// Deal impact damage.
	if ( !( impactFlags & MIF_NO_DAMAGE ) )
	{
		if ( ent->damage && G_Alive( hitEnt ) )
		{
			vec3_t dir;

			BG_EvaluateTrajectoryDelta( &ent->s.pos, level.time, dir );

			if ( VectorNormalize( dir ) == 0 )
			{
				dir[ 2 ] = 1; // stepped on a grenade
			}

			int dflags = 0;
			if ( !ma->doLocationalDamage ) dflags |= DAMAGE_NO_LOCDAMAGE;
			if ( ma->doKnockback )         dflags |= DAMAGE_KNOCKBACK;

			hitEnt->entity->Damage(ent->damage * MissileTimeDmgMod(ent), attacker,
			                       Vec3::Load(trace->endpos), Vec3::Load(dir), dflags,
			                       (meansOfDeath_t)ent->methodOfDeath);
		}

		// splash damage (doesn't apply to person directly hit)
		if ( ent->splashDamage )
		{
			G_RadiusDamage( trace->endpos, ent->parent,
			                ent->splashDamage * MissileTimeSplashDmgMod( ent ),
			                ent->splashRadius, hitEnt, ( ma->doKnockback ? DAMAGE_KNOCKBACK : 0 ),
			                ent->splashMethodOfDeath );
		}
	}

	// Play hit effects and remove the missile.
	if ( !( impactFlags & MIF_NO_EFFECT ) )
	{
		// Use either the trajectory direction or the surface normal for the hit event.
		if ( ma->impactFlightDirection )
		{
			vec3_t trajDir;
			BG_EvaluateTrajectoryDelta( &ent->s.pos, level.time, trajDir );
			VectorNormalize( trajDir );
			dirAsByte = DirToByte( trajDir );
		}
		else
		{
			dirAsByte = DirToByte( trace->plane.normal );
		}

		// Add hit event.
		if ( HasComponents<HealthComponent>(*hitEnt->entity) )
		{
			G_AddEvent( ent, EV_MISSILE_HIT_ENTITY, dirAsByte );

			ent->s.otherEntityNum = hitEnt->s.number;
		}
		else if ( trace->surfaceFlags & SURF_METAL )
		{
			G_AddEvent( ent, EV_MISSILE_HIT_METAL, dirAsByte );
		}
		else
		{
			G_AddEvent( ent, EV_MISSILE_HIT_ENVIRONMENT, dirAsByte );
		}

		ent->freeAfterEvent = true;

		// HACK: Change over to a general entity at the point of impact.
		ent->s.eType = ET_GENERAL;

		// Prevent map models from appearing at impact point.
		ent->s.modelindex = 0;

		// Save net bandwith.
		G_SnapVectorTowards( trace->endpos, ent->s.pos.trBase );

		G_SetOrigin( ent, trace->endpos );

		trap_LinkEntity( ent );
	}
	// If no impact happened, check if we should continue or free ourselves.
	else if ( !( impactFlags & MIF_NO_FREE ) )
	{
		G_FreeEntity( ent );
	}
}
예제 #7
0
void SpikerComponent::Think(int timeDelta) {
	// Don't act if recovering from shot or disabled.
	if (!GetAlienBuildableComponent().GetBuildableComponent().Active() || level.time < restUntil) {
		lastExpectedDamage = 0.0f;
		lastSensing = false;

		return;
	}

	float expectedDamage = 0.0f;
	bool  sensing = false;

	// Calculate expected damage to decide on the best moment to shoot.
	ForEntities<HealthComponent>([&](Entity& other, HealthComponent& healthComponent) {
		if (G_Team(other.oldEnt) == TEAM_NONE)                            return;
		if (G_OnSameTeam(entity.oldEnt, other.oldEnt))                    return;
		if ((other.oldEnt->flags & FL_NOTARGET))                          return;
		if (!healthComponent.Alive())                                     return;
		if (G_Distance(entity.oldEnt, other.oldEnt) > SPIKE_RANGE)        return;
		if (other.Get<BuildableComponent>())                              return;
		if (!G_LineOfSight(entity.oldEnt, other.oldEnt))                  return;

		Vec3 dorsal    = Vec3::Load(entity.oldEnt->s.origin2);
		Vec3 toTarget  = Vec3::Load(other.oldEnt->s.origin) - Vec3::Load(entity.oldEnt->s.origin);
		Vec3 otherMins = Vec3::Load(other.oldEnt->r.mins);
		Vec3 otherMaxs = Vec3::Load(other.oldEnt->r.maxs);

		// With a straight shot, only entities in the spiker's upper hemisphere can be hit.
		// Since the spikes obey gravity, increase or decrease this radius of damage by up to
		// GRAVITY_COMPENSATION_ANGLE degrees depending on the spiker's orientation.
		if (Math::Dot(Math::Normalize(toTarget), dorsal) < gravityCompensation) return;

		// Approximate average damage the entity would receive from spikes.
		const missileAttributes_t* ma = BG_Missile(MIS_SPIKER);
		float spikeDamage  = ma->damage;
		float distance     = Math::Length(toTarget);
		float bboxDiameter = Math::Length(otherMins) + Math::Length(otherMaxs);
		float bboxEdge     = (1.0f / M_ROOT3) * bboxDiameter; // Assumes a cube.
		float hitEdge      = bboxEdge + ((1.0f / M_ROOT3) * ma->size); // Add half missile edge.
		float hitArea      = hitEdge * hitEdge; // Approximate area resulting in a hit.
		float effectArea   = 2.0f * M_PI * distance * distance; // Area of a half sphere.
		float damage       = (hitArea / effectArea) * (float)MISSILES * spikeDamage;

		// Sum up expected damage for all targets, regardless of whether they are in sense range.
		expectedDamage += damage;

		// Start sensing (frequent search for best moment to shoot) as soon as an enemy that can be
		// damaged is close enough. Note that the Spiker will shoot eventually after it started
		// sensing, and then returns to a less alert state.
		if (distance < SPIKER_SENSE_RANGE && !sensing) {
			sensing = true;

			if (!lastSensing) {
				logger.Verbose("Spiker #%i now senses an enemy and will check more frequently for "
					"the best moment to shoot.", entity.oldEnt->s.number);

				RegisterFastThinker();
			}
		}
	});

	bool senseLost = lastSensing && !sensing;

	if (sensing || senseLost) {
		bool lessDamage = (expectedDamage <= lastExpectedDamage);
		bool enoughDamage = (expectedDamage >= DAMAGE_THRESHOLD);

		if (sensing) {
			logger.Verbose("Spiker #%i senses an enemy and expects to do %.1f damage.%s%s",
				entity.oldEnt->s.number, expectedDamage, (lessDamage && !enoughDamage)
				? " This has not increased, so it's time to shoot." : "", enoughDamage ?
				" This is already enough, shoot now." : "");
		}

		if (senseLost) {
			logger.Verbose("Spiker #%i lost track of all enemies after expecting to do %.1f damage."
				" This makes the spiker angry, so it will shoot anyway.",
				entity.oldEnt->s.number, lastExpectedDamage);
		}

		// Shoot when
		// - a threshold was reached by the expected damage, implying a very close enemy,
		// - the expected damage has decreased, witnessing a recent local maximum, or
		// - whenever all viable targets have left the sense range.
		// The first trigger plays around the delay in sensing a local maximum and in having the
		// spikes travel towards their destination.
		// The last trigger guarantees that the spiker always shoots eventually after sensing.
		if (enoughDamage || (sensing && lessDamage) || senseLost) {
			Fire();
		}
	}

	lastExpectedDamage = expectedDamage;
	lastSensing = sensing;
}
예제 #8
0
bool SpikerComponent::Fire() {
	gentity_t *self = entity.oldEnt;
	// Check if still resting.
	if (restUntil > level.time) {
		logger.Verbose("Spiker #%i wanted to fire but wasn't ready.", entity.oldEnt->s.number);

		return false;
	} else {
		logger.Verbose("Spiker #%i is firing!", entity.oldEnt->s.number);
	}

	// Play shooting animation.
	G_SetBuildableAnim(self, BANIM_ATTACK1, false);
	GetBuildableComponent().ProtectAnimation(5000);

	// TODO: Add a particle effect.
	//G_AddEvent(self, EV_ALIEN_SPIKER, DirToByte(self->s.origin2));

	// Calculate total perimeter of all spike rows to allow for a more even spike distribution.
	// A "row" is a group of missile launch directions with a common base altitude (angle measured
	// from the Spiker's horizon to its zenith) which is slightly adjusted for each new missile in
	// the row (at most halfway to the base altitude of a neighbouring row).
	float totalPerimeter = 0.0f;
	for (int row = 0; row < MISSILEROWS; row++) {
		float rowAltitude = (((float)row + 0.5f) * M_PI_2) / (float)MISSILEROWS;
		float rowPerimeter = 2.0f * M_PI * cos(rowAltitude);
		totalPerimeter += rowPerimeter;
	}

	// TODO: Use new vector library.
	vec3_t dir, zenith, rotAxis;

	// As rotation axis for setting the altitude, any vector perpendicular to the zenith works.
	VectorCopy(self->s.origin2, zenith);
	PerpendicularVector(rotAxis, zenith);

	// Distribute and launch missiles.
	for (int row = 0; row < MISSILEROWS; row++) {
		// Set the base altitude and get the perimeter for the current row.
		float rowAltitude = (((float)row + 0.5f) * M_PI_2) / (float)MISSILEROWS;
		float rowPerimeter = 2.0f * M_PI * cos(rowAltitude);

		// Attempt to distribute spikes with equal expected angular distance on all rows.
		int spikes = (int)round(((float)MISSILES * rowPerimeter) / totalPerimeter);

		// Launch missiles in the current row.
		for (int spike = 0; spike < spikes; spike++) {
			float spikeAltitude = rowAltitude + (0.5f * crandom() * M_PI_2 / (float)MISSILEROWS);
			float spikeAzimuth = 2.0f * M_PI * (((float)spike + 0.5f * crandom()) / (float)spikes);

			// Set launch direction altitude.
			RotatePointAroundVector(dir, rotAxis, zenith, RAD2DEG(M_PI_2 - spikeAltitude));

			// Set launch direction azimuth.
			RotatePointAroundVector(dir, zenith, dir, RAD2DEG(spikeAzimuth));

			// Trace in the shooting direction and do not shoot spikes that are likely to harm
			// friendly entities.
			bool fire = SafeToShoot(Vec3::Load(dir));

			logger.Debug("Spiker #%d %s: Row %d/%d: Spike %2d/%2d: "
				"( Alt %2.0f°, Az %3.0f° → %.2f, %.2f, %.2f )", self->s.number,
				fire ? "fires" : "skips", row + 1, MISSILEROWS, spike + 1, spikes,
				RAD2DEG(spikeAltitude), RAD2DEG(spikeAzimuth), dir[0], dir[1], dir[2]);

			if (!fire) {
				continue;
			}

			G_SpawnMissile(
				MIS_SPIKER, self, self->s.origin, dir, nullptr, G_FreeEntity,
				level.time + (int)(1000.0f * SPIKE_RANGE / (float)BG_Missile(MIS_SPIKER)->speed));
		}
	}

	restUntil = level.time + COOLDOWN;
	RegisterSlowThinker();

	return true;
}