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."); } } }
// 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; } }
/** * @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); }
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)); }
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; }
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; }
void TurretComponent::ResetPitch() { Vec3 targetRelativeAngles = relativeAimAngles; targetRelativeAngles[PITCH] = 0.0f; directionToTarget = RelativeAnglesToDirection(targetRelativeAngles); turretLogger.Debug("Target pitch reset. New direction: %s.", directionToTarget); }
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); } }
/** * @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."); }
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); } }); }
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(); }
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()); } }
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; }
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); }