Exemplo n.º 1
0
float ArmorComponent::GetLocationalDamageMod(float angle, float height) {
	class_t pcl = (class_t)entity.oldEnt->client->ps.stats[STAT_CLASS];

	bool crouching = (entity.oldEnt->client->ps.pm_flags & PMF_DUCKED);

	for (int regionNum = 0; regionNum < g_numDamageRegions[pcl]; regionNum++) {
		damageRegion_t *region = &g_damageRegions[pcl][regionNum];

		// Ignore the non-locational pseudo region.
		if (region->nonlocational) continue;

		// Crouch state must match.
		if (region->crouch != crouching) continue;

		// Height must be within given range.
		if (height < region->minHeight || height > region->maxHeight) continue;

		// Angle must be within given range.
		if ((region->minAngle <= region->maxAngle && (angle < region->minAngle || angle > region->maxAngle)) ||
		    (region->minAngle >  region->maxAngle && (angle > region->maxAngle && angle < region->minAngle))) {
			continue;
		}

		armorLogger.Debug("Locational damage modifier of %.2f found for angle %.2f and height %.2f (%s).",
		                  region->modifier, angle, height, region->name);

		return region->modifier;
	}

	armorLogger.Debug("Locational damage modifier for angle %.2f and height %.2f not found.",
	                  angle, height);

	return 1.0f;
}
void IgnitableComponent::HandleIgnite(gentity_t* fireStarter) {
	if (!fireStarter) {
		// TODO: Find out why this happens.
		fireLogger.Notice("Received ignite message with no fire starter.");
	}

	if (level.time < immuneUntil) {
		fireLogger.Debug("Not ignited: Immune against fire.");

		return;
	}

	// Refresh ignite time even if already burning.
	igniteTime = level.time;

	if (!onFire) {
		onFire = true;
		this->fireStarter = fireStarter;

		fireLogger.Debug("Ignited.");
	} else {
		if (alwaysOnFire && !this->fireStarter) {
			// HACK: Igniting an alwaysOnFire entity will initialize the fire starter.
			this->fireStarter = fireStarter;

			fireLogger.Debug("Firestarter initialized.");
		} else {
			fireLogger.Debug("Re-Ignited.");
		}
	}
}
Exemplo n.º 3
0
// TODO: Move credits array to HealthComponent.
void HealthComponent::ScaleDamageAccounts(float healthRestored) {
	if (healthRestored <= 0.0f) return;

	// Get total damage account and remember relevant clients.
	float totalAccreditedDamage = 0.0f;
	std::vector<Entity*> relevantClients;
	ForEntities<ClientComponent>([&](Entity& other, ClientComponent& client) {
		float clientDamage = entity.oldEnt->credits[other.oldEnt->s.number].value;
		if (clientDamage > 0.0f) {
			totalAccreditedDamage += clientDamage;
			relevantClients.push_back(&other);
		}
	});

	if (relevantClients.empty()) return;

	// Calculate account scale factor.
	float scale;
	if (healthRestored < totalAccreditedDamage) {
		scale = (totalAccreditedDamage - healthRestored) / totalAccreditedDamage;

		healthLogger.Debug("Scaling damage accounts of %i client(s) by %.2f.",
		                   relevantClients.size(), scale);
	} else {
		// Clear all accounts.
		scale = 0.0f;

		healthLogger.Debug("Clearing damage accounts of %i client(s).", relevantClients.size());
	}

	// Scale down or clear damage accounts.
	for (Entity* other : relevantClients) {
		entity.oldEnt->credits[other->oldEnt->s.number].value *= scale;
	}
}
void IgnitableComponent::ConsiderStop(int timeDelta) {
	if (!onFire) return;

	// Don't stop freshly (re-)ignited fires.
	if (igniteTime + MIN_BURN_TIME > level.time) {
		fireLogger.Debug("(Re-)Ignited %i ms ago, skipping stop check.", level.time - igniteTime);

		return;
	}

	float burnStopChance = STOP_CHANCE;

	// Lower burn stop chance if there are other burning entities nearby.
	ForEntities<IgnitableComponent>([&](Entity &other, IgnitableComponent &ignitable){
		if (&other == &entity) return;
		if (!ignitable.onFire) return;
		if (G_Distance(other.oldEnt, entity.oldEnt) > STOP_RADIUS) return;

		float frac = G_Distance(entity.oldEnt, other.oldEnt) / STOP_RADIUS;
		float mod  = frac * 1.0f + (1.0f - frac) * STOP_CHANCE;

		burnStopChance *= mod;
	});

	// Attempt to stop burning.
	if (random() < burnStopChance) {
		fireLogger.Debug("Stopped burning (chance was %.0f%%)", burnStopChance * 100.0f);

		entity.Extinguish(0);
		return;
	} else {
		fireLogger.Debug("Didn't stop burning (chance was %.0f%%)", burnStopChance * 100.0f);
	}
}
void IgnitableComponent::ConsiderStop(int timeDelta) {
	if (!onFire) return;

	// Don't stop freshly (re-)ignited fires.
	if (igniteTime + MIN_BURN_TIME > level.time) {
		fireLogger.DoDebugCode([&]{
			int elapsed   = level.time - igniteTime;
			int remaining = MIN_BURN_TIME - elapsed;
			fireLogger.Debug("Burning for %.1fs, skipping stop check for another %.1fs.",
			                 (float)elapsed/1000.0f, (float)remaining/1000.0f);
		});

		return;
	}

	float averagePostMinBurnTime = BASE_AVERAGE_BURN_TIME - MIN_BURN_TIME;

	// Increase average burn time dynamically for burning entities in range.
	ForEntities<IgnitableComponent>([&](Entity &other, IgnitableComponent &ignitable){
		if (&other == &entity) return;
		if (!ignitable.onFire) return;

		// TODO: Use LocationComponent.
		float distance = G_Distance(other.oldEnt, entity.oldEnt);

		if (distance > EXTRA_BURN_TIME_RADIUS) return;

		float distanceFrac = distance / EXTRA_BURN_TIME_RADIUS;
		float distanceMod  = 1.0f - distanceFrac;

		averagePostMinBurnTime += EXTRA_AVERAGE_BURN_TIME * distanceMod;
	});

	// The burn stop chance follows an exponential distribution.
	float lambda = 1.0f / averagePostMinBurnTime;
	float burnStopChance = 1.0f - std::exp(-1.0f * lambda * (float)timeDelta);

	float averageTotalBurnTime = averagePostMinBurnTime + (float)MIN_BURN_TIME;

	// Attempt to stop burning.
	if (random() < burnStopChance) {
		fireLogger.Notice("Stopped burning after %.1fs, target average lifetime was %.1fs.",
		                  (float)(level.time - igniteTime) / 1000.0f, averageTotalBurnTime / 1000.0f);

		entity.Extinguish(0);
		return;
	} else {
		fireLogger.Debug("Burning for %.1fs, target average lifetime is %.1fs.",
		                 (float)(level.time - igniteTime) / 1000.0f, averageTotalBurnTime / 1000.0f);
	}
}
void IgnitableComponent::HandleIgnite(gentity_t* fireStarter) {
	if (!fireStarter) {
		// TODO: Find out why this happens.
		fireLogger.Notice("Received ignite message with no fire starter.");
	}

	if (level.time < immuneUntil) {
		fireLogger.Debug("Not ignited: Immune against fire.");

		return;
	}

	// Start burning on initial ignition.
	if (!onFire) {
		onFire = true;
		this->fireStarter = fireStarter;

		fireLogger.Notice("Ignited.");
	} else {
		if (alwaysOnFire && !this->fireStarter) {
			// HACK: Igniting an alwaysOnFire entity will initialize the fire starter.
			this->fireStarter = fireStarter;

			fireLogger.Debug("Firestarter set.");
		} else {
			fireLogger.Debug("Re-ignited.");
		}
	}

	// Refresh ignite time even if already burning.
	igniteTime = level.time;

	// The spread delay follows a normal distribution: More likely to spread early than late.
	int spreadTarget = level.time + (int)std::abs(normalDistribution(randomGenerator));

	// Allow re-ignition to update the spread delay to a lower value.
	if (spreadTarget < spreadAt) {
		fireLogger.DoNoticeCode([&]{
			int newDelay = spreadTarget - level.time;
			if (spreadAt == INT_MAX) {
				fireLogger.Notice("Spread delay set to %.1fs.", newDelay * 0.001f);
			} else {
				int oldDelay = spreadAt - level.time;
				fireLogger.Notice("Spread delay updated from %.1fs to %.1fs.",
				                  oldDelay * 0.001f, newDelay * 0.001f);
			}
		});
		spreadAt = spreadTarget;
	}
}
Exemplo n.º 7
0
/**
 * @brief Predict the total efficiency gain for a team when a miner is constructed at a given point.
 * @return Predicted efficiency delta in percent points.
 * @todo Consider RGS set for deconstruction.
 */
float G_RGSPredictEfficiencyDelta(vec3_t origin, team_t team) {
	float delta = G_RGSPredictOwnEfficiency(origin);

	buildpointLogger.Debug("Predicted efficiency of new miner itself: %f.", delta);

	ForEntities<MiningComponent>([&] (Entity& miner, MiningComponent& miningComponent) {
		if (G_Team(miner.oldEnt) != team) return;

		delta += RGSPredictEfficiencyLoss(miner, origin);
	});

	buildpointLogger.Debug("Predicted efficiency delta: %f. Build point delta: %f.", delta,
	                       delta * g_buildPointBudgetPerMiner.value);

	return delta;
}
void AlienBuildableComponent::HandleDie(gentity_t* killer, meansOfDeath_t meansOfDeath) {
	entity.oldEnt->powered = false;

	// Warn if in main base and there's an overmind.
	gentity_t *om;
	if ((om = G_ActiveOvermind()) && om != entity.oldEnt && level.time > om->warnTimer
			&& G_InsideBase(entity.oldEnt, true) && G_IsWarnableMOD(meansOfDeath)) {
		om->warnTimer = level.time + ATTACKWARN_NEARBY_PERIOD;
		G_BroadcastEvent(EV_WARN_ATTACK, 0, TEAM_ALIENS);
		Beacon::NewArea(BCT_DEFEND, entity.oldEnt->s.origin, entity.oldEnt->buildableTeam);
	}

	// Set blast timer.
	int blastDelay = 0;
	if (entity.oldEnt->spawned && GetBuildableComponent().GetHealthComponent().Health() /
			GetBuildableComponent().GetHealthComponent().MaxHealth() > -1.0f) {
		blastDelay += GetBlastDelay();
	}

	alienBuildableLogger.Debug("Alien buildable dies, will blast in %i ms.", blastDelay);

	GetBuildableComponent().SetState(BuildableComponent::PRE_BLAST);

	GetBuildableComponent().REGISTER_THINKER(Blast, ThinkingComponent::SCHEDULER_BEFORE, blastDelay);
}
Exemplo n.º 9
0
void HealthComponent::SetMaxHealth(float maxHealth, bool scaleHealth) {
	ASSERT_GT(maxHealth, 0.0f);

	healthLogger.Debug("Changing maximum health: %3.1f → %3.1f.", this->maxHealth, maxHealth);

	HealthComponent::maxHealth = maxHealth;
	if (scaleHealth) SetHealth(health * (this->maxHealth / maxHealth));
}
Exemplo n.º 10
0
void HealthComponent::SetHealth(float health) {
	Math::Clamp(health, FLT_EPSILON, maxHealth);

	healthLogger.Debug("Changing health: %3.1f → %3.1f.", this->health, health);

	ScaleDamageAccounts(health - this->health);
	HealthComponent::health = health;
}
Exemplo n.º 11
0
float ArmorComponent::GetNonLocationalDamageMod() {
	class_t pcl = (class_t)entity.oldEnt->client->ps.stats[STAT_CLASS];

	for (int regionNum = 0; regionNum < g_numDamageRegions[pcl]; regionNum++) {
		damageRegion_t *region = &g_damageRegions[pcl][regionNum];

		if (!region->nonlocational) continue;

		armorLogger.Debug("Found non-locational damage modifier of %.2f.", region->modifier);

		return region->modifier;
	}

	armorLogger.Debug("No non-locational damage modifier found.");

	return 1.0f;
}
Exemplo n.º 12
0
void TurretComponent::ResetPitch() {
	Vec3 targetRelativeAngles = relativeAimAngles;
	targetRelativeAngles[PITCH] = 0.0f;

	directionToTarget = RelativeAnglesToDirection(targetRelativeAngles);

	turretLogger.Debug("Target pitch reset. New direction: %s.", directionToTarget);
}
Exemplo n.º 13
0
void TurretComponent::LowerPitch() {
	Vec3 targetRelativeAngles = relativeAimAngles;
	targetRelativeAngles[PITCH] = PITCH_CAP;

	directionToTarget = RelativeAnglesToDirection(targetRelativeAngles);

	turretLogger.Debug("Target pitch lowered. New direction: %s.", directionToTarget);
}
void IgnitableComponent::DamageSelf(int timeDelta) {
	if (!onFire) return;

	float damage = SELF_DAMAGE * timeDelta * 0.001f;

	if (entity.Damage(damage, fireStarter, {}, {}, 0, MOD_BURN)) {
		fireLogger.Debug("Self burn damage of %.1f (%.1f/s) was dealt.", damage, SELF_DAMAGE);
	}
}
void IgnitableComponent::DamageArea(int timeDelta) {
	if (!onFire) return;

	float damage = SPLASH_DAMAGE * timeDelta * 0.001f;

	if (G_SelectiveRadiusDamage(entity.oldEnt->s.origin, fireStarter, damage, SPLASH_DAMAGE_RADIUS,
			entity.oldEnt, MOD_BURN, TEAM_NONE)) {
		fireLogger.Debug("Area burn damage of %.1f (%.1f/s) was dealt.", damage, SPLASH_DAMAGE);
	}
}
Exemplo n.º 16
0
/**
 * @brief Predict the efficiecy loss of an existing miner if another one is constructed closeby.
 * @return Efficiency loss as negative value.
 */
static float RGSPredictEfficiencyLoss(Entity& miner, vec3_t newMinerOrigin) {
	float distance               = Distance(miner.oldEnt->s.origin, newMinerOrigin);
	float oldPredictedEfficiency = miner.Get<MiningComponent>()->Efficiency(true);
	float newPredictedEfficiency = oldPredictedEfficiency * MiningComponent::InterferenceMod(distance);
	float efficiencyLoss         = newPredictedEfficiency - oldPredictedEfficiency;

	buildpointLogger.Debug("Predicted efficiency loss of existing miner: %f - %f = %f.",
	                       oldPredictedEfficiency, newPredictedEfficiency, efficiencyLoss);

	return efficiencyLoss;
}
void IgnitableComponent::HandleExtinguish(int immunityTime) {
	if (!onFire) return;

	onFire = false;
	immuneUntil = level.time + immunityTime;

	if (alwaysOnFire) {
		entity.FreeAt(DeferredFreeingComponent::FREE_BEFORE_THINKING);
	}

	fireLogger.Debug("Extinguished.");
}
Exemplo n.º 18
0
void HealthComponent::HandleHeal(float amount, gentity_t* source) {
	if (health <= 0.0f) return;
	if (health >= maxHealth) return;

	// Only heal up to maximum health.
	amount = std::min(amount, maxHealth - health);

	if (amount <= 0.0f) return;

	healthLogger.Debug("Healing: %3.1f (%3.1f → %3.1f)", amount, health, health + amount);

	health += amount;
	ScaleDamageAccounts(amount);
}
void IgnitableComponent::ConsiderSpread(int timeDelta) {
	if (!onFire) return;

	ForEntities<IgnitableComponent>([&](Entity &other, IgnitableComponent &ignitable){
		if (&other == &entity) return;

		// TODO: Use LocationComponent.
		float chance = 1.0f - G_Distance(entity.oldEnt, other.oldEnt) / SPREAD_RADIUS;

		if (chance <= 0.0f) return; // distance > spread radius

		if (random() < chance) {
			if (G_LineOfSight(entity.oldEnt, other.oldEnt) && other.Ignite(fireStarter)) {
				fireLogger.Debug("(Re-)Ignited a neighbour (chance was %.0f%%)", chance * 100.0f);
			} else {
				fireLogger.Debug("Tried to ignite a non-ignitable or non-LOS neighbour (chance was %.0f%%)",
								 chance * 100.0f);
			}
		} else {
			fireLogger.Debug("Didn't try to ignite a neighbour (chance was %.0f%%)", chance * 100.0f);
		}
	});
}
Exemplo n.º 20
0
void TurretComponent::TrackEntityTarget() {
	if (!target) return;

	Vec3 oldDirectionToTarget = directionToTarget;

	Vec3 targetOrigin = Vec3::Load(target->s.origin);
	Vec3 muzzle       = Vec3::Load(entity.oldEnt->s.pos.trBase);

	directionToTarget = Math::Normalize(targetOrigin - muzzle);

	if (Math::DistanceSq(directionToTarget, oldDirectionToTarget) > 0.0f) {
		turretLogger.Debug("Following an entity target. New direction: %s.", directionToTarget);
	}
}
// TODO: Consider location as well as direction when both given.
void KnockbackComponent::HandleDamage(float amount, gentity_t* source, Util::optional<Vec3> location,
                                      Util::optional<Vec3> direction, int flags, meansOfDeath_t meansOfDeath) {
	if (!(flags & DAMAGE_KNOCKBACK)) return;
	if (amount <= 0.0f) return;

	if (!direction) {
		knockbackLogger.Warn("Received damage message with knockback flag set but no direction.");
		return;
	}

	if (Math::Length(direction.value()) == 0.0f) {
		knockbackLogger.Warn("Attempt to do knockback with null vector direction.");
		return;
	}

	// TODO: Remove dependency on client.
	gclient_t *client = entity.oldEnt->client;
	assert(client);

	// Check for immunity.
	if (client->noclip) return;
	if (client->sess.spectatorState != SPECTATOR_NOT) return;

	float mass = (float)BG_Class(client->ps.stats[ STAT_CLASS ])->mass;

	if (mass <= 0.0f) {
		knockbackLogger.Warn("Attempt to do knockback against target with no mass, assuming normal mass.");
		mass = KNOCKBACK_NORMAL_MASS;
	}

	float massMod  = Math::Clamp(KNOCKBACK_NORMAL_MASS / mass, KNOCKBACK_MIN_MASSMOD, KNOCKBACK_MAX_MASSMOD);
	float strength = amount * DAMAGE_TO_KNOCKBACK * massMod;

	// Change client velocity.
	Vec3 clientVelocity = Vec3::Load(client->ps.velocity);
	clientVelocity += Math::Normalize(direction.value()) * strength;
	clientVelocity.Store(client->ps.velocity);

	// Set pmove timer so that the client can't cancel out the movement immediately.
	if (!client->ps.pm_time) {
		client->ps.pm_time = KNOCKBACK_PMOVE_TIME;
		client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
	}

	knockbackLogger.Debug("Knockback: client: %i, strength: %.1f (massMod: %.1f).",
	                      entity.oldEnt->s.number, strength, massMod);
}
// TODO: Move this to the client side.
void AlienBuildableComponent::CreepRecede(int timeDelta) {
	alienBuildableLogger.Debug("Starting creep recede.");

	G_AddEvent(entity.oldEnt, EV_BUILD_DESTROY, 0);

	if (entity.oldEnt->spawned) {
		entity.oldEnt->s.time = -level.time;
	} else {
		entity.oldEnt->s.time = -(level.time - (int)(
			(float)CREEP_SCALEDOWN_TIME *
			(1.0f - ((float)(level.time - entity.oldEnt->creationTime) /
					 (float)BG_Buildable(entity.oldEnt->s.modelindex)->buildTime)))
		);
	}

	// Remove buildable when done.
	GetBuildableComponent().REGISTER_THINKER(Remove, ThinkingComponent::SCHEDULER_AFTER, CREEP_SCALEDOWN_TIME);
	GetBuildableComponent().GetThinkingComponent().UnregisterActiveThinker();
}
Exemplo n.º 23
0
void SV_ExecuteClientCommand( client_t *cl, const char *s, bool clientOK, bool premaprestart )
{
	ucmd_t   *u;
	bool bProcessed = false;

	Log::Debug( "EXCL: %s", s );
	Cmd::Args args(s);

	if (args.Argc() == 0) {
		return;
	}

    clientCommands.Debug("Client %s sent command '%s'", cl->name, s);
	for (u = ucmds; u->name; u++) {
		if (args.Argv(0) == u->name) {
			if (premaprestart && !u->allowedpostmapchange) {
				continue;
			}

			u->func(cl, args);
			bProcessed = true;
			break;
		}
	}

	if ( clientOK )
	{
		// pass unknown strings to the game
		if ( !u->name && sv.state == serverState_t::SS_GAME )
		{
			gvm.GameClientCommand( cl - svs.clients, s );
		}
	}
	else if ( !bProcessed )
	{
		Log::Debug( "client text ignored for %s^7: %s", cl->name, args.Argv(0).c_str());
	}
}
Exemplo n.º 24
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;
}
Exemplo n.º 25
0
bool TurretComponent::MoveHeadToTarget(int timeDelta) {
	// Note that a timeDelta of zero may happen on a first thinker execution.
	// We do not return in that case since we don't know the return value yet.
	ASSERT_GE(timeDelta, 0);

	float timeMod = (float)timeDelta / 1000.0f;

	// Compute maximum angle changes for this execution.
	Vec3 maxAngleChange;
	maxAngleChange[PITCH] = timeMod * PITCH_SPEED;
	maxAngleChange[YAW]   = timeMod * YAW_SPEED;
	maxAngleChange[ROLL]  = 0.0f;

	// Compute angles to target, relative to the turret's base.
	Vec3 relativeAnglesToTarget = DirectionToRelativeAngles(directionToTarget);

	// Compute difference between angles to target and current angles.
	Vec3 deltaAngles;
	AnglesSubtract(relativeAnglesToTarget.Data(), relativeAimAngles.Data(), deltaAngles.Data());

	// Stop if there is nothing to do.
	if (Math::Length(deltaAngles) < 0.1f) {
		return true;
	}

	bool targetReached = true;
	Vec3 oldRelativeAimAngles = relativeAimAngles;

	// Adjust aim angles towards target angles.
	for (int angle = 0; angle < 3; angle++) {
		if (angle == ROLL) continue;

		if (fabs(deltaAngles[angle]) > maxAngleChange[angle]) {
			relativeAimAngles[angle] += (deltaAngles[angle] < 0.0f)
				? -maxAngleChange[angle]
				:  maxAngleChange[angle];
			targetReached = false;
		} else {
			relativeAimAngles[angle] = relativeAnglesToTarget[angle];
		}
	}

	// Respect pitch limits.
	if (relativeAimAngles[PITCH] > PITCH_CAP) {
		relativeAimAngles[PITCH] = PITCH_CAP;
		targetReached = false;
	}

	if (Math::DistanceSq(oldRelativeAimAngles, relativeAimAngles) > 0.0f) {
		turretLogger.Debug(
			"Aiming. Elapsed: %d ms. Delta: %.2f. Max: %.2f. Old: %s. New: %s. Reached: %s.",
			timeDelta, deltaAngles, maxAngleChange, oldRelativeAimAngles, relativeAimAngles, targetReached
		);
	}

	// TODO: Move gentity_t.buildableAim to BuildableComponent.
	Vec3 absoluteAimAngles = RelativeAnglesToAbsoluteAngles(relativeAimAngles);
	absoluteAimAngles.Store(entity.oldEnt->buildableAim);

	return targetReached;
}
void AlienBuildableComponent::Remove(int timeDelta) {
	alienBuildableLogger.Debug("Removing alien buildable.");

	entity.FreeAt(DeferredFreeingComponent::FREE_AFTER_THINKING);
}