bool TurretComponent::TargetValid(Entity& target, bool newTarget) {
	if (!target.Get<ClientComponent>() ||
	    target.Get<SpectatorComponent>() ||
	    Entities::IsDead(target) ||
	    (target.oldEnt->flags & FL_NOTARGET) ||
	    !Entities::OnOpposingTeams(entity, target) ||
	    G_Distance(entity.oldEnt, target.oldEnt) > range ||
	    !trap_InPVS(entity.oldEnt->s.origin, target.oldEnt->s.origin)) {

		if (!newTarget) {
			turretLogger.Verbose("Target lost: Out of range or eliminated.");
		}

		return false;
	}

	// New targets require a line of sight.
	if (G_LineOfFire(entity.oldEnt, target.oldEnt)) {
		lastLineOfSightToTarget = level.time;
	} else if (newTarget) {
		return false;
	}

	// Give up on an existing target if there was no line of sight for a while.
	if (lastLineOfSightToTarget + GIVEUP_TARGET_TIME <= level.time) {
		turretLogger.Verbose("Giving up on target: No line of sight for %d ms.",
			level.time - lastLineOfSightToTarget
		);

		return false;
	}

	return true;
}
void TurretComponent::ResetDirection() {
	directionToTarget = baseDirection;

	turretLogger.Verbose("Target direction reset. New direction: %s.",
		Utility::Print(directionToTarget)
	);
}
void TurretComponent::RemoveTarget() {
	if (target) {
		// TODO: Decrease tracked-by counter for the target.

		turretLogger.Verbose("Target removed.");
	}

	target = nullptr;
	lastLineOfSightToTarget = 0;
}
Example #4
0
void SpikerComponent::HandleDamage(float amount, gentity_t *source, Util::optional<Vec3> location,
                                   Util::optional<Vec3> direction, int flags, meansOfDeath_t meansOfDeath) {
	if (!GetAlienBuildableComponent().GetBuildableComponent().Active()) {
		return;
	}

	// Shoot if there is a viable target.
	if (lastExpectedDamage > 0.0f) {
		logger.Verbose("Spiker #%i was hurt while an enemy is close enough to also get hurt, so "
			"go eye for an eye.", entity.oldEnt->s.number);

		Fire();
	}
}
Entity* TurretComponent::FindEntityTarget(std::function<bool(Entity&, Entity&)> CompareTargets) {
	// Delete old target.
	RemoveTarget();

	// Search best target.
	// TODO: Iterate over all valid targets, do not assume they have to be clients.
	ForEntities<ClientComponent>([&](Entity& candidate, ClientComponent& clientComponent) {
		if (TargetValid(candidate, true)) {
			if (!target || CompareTargets(candidate, *target->entity)) {
				target = candidate.oldEnt;
			}
		}
	});

	if (target) {
		// TODO: Increase tracked-by counter for a new target.

		turretLogger.Verbose("Target acquired.");
	}

	return target ? target->entity : nullptr;
}
void TurretComponent::SetBaseDirection() {
	vec3_t torsoDirectionOldVec;
	AngleVectors(entity.oldEnt->s.angles, torsoDirectionOldVec, nullptr, nullptr);

	Vec3 torsoDirection = Math::Normalize(Vec3::Load(torsoDirectionOldVec));
	Vec3 traceStart     = Vec3::Load(entity.oldEnt->s.pos.trBase);
	Vec3 traceEnd       = traceStart + MINIMUM_CLEARANCE * torsoDirection;

	trace_t tr;
	trap_Trace(&tr, traceStart.Data(), nullptr, nullptr, traceEnd.Data(), entity.oldEnt->s.number,
	           MASK_SHOT, 0);

	// TODO: Check the presence of a PhysicsComponent to decide whether the obstacle is permanent.
	if (tr.entityNum == ENTITYNUM_WORLD ||
	    g_entities[tr.entityNum].entity->Get<BuildableComponent>()) {
		baseDirection = -torsoDirection;
	} else {
		baseDirection =  torsoDirection;
	}

	turretLogger.Verbose("Base direction set to %s.", baseDirection);
}
Example #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;
}
Example #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;
}