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; }
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); }
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; }
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; }