void IgnitableComponent::ConsiderSpread(int timeDelta) { if (!onFire) return; if (level.time < spreadAt) return; fireLogger.Notice("Trying to spread."); ForEntities<IgnitableComponent>([&](Entity &other, IgnitableComponent &ignitable){ if (&other == &entity) return; // Don't re-ignite. if (ignitable.onFire) return; // TODO: Use LocationComponent. float distance = G_Distance(other.oldEnt, entity.oldEnt); if (distance > SPREAD_RADIUS) return; float distanceFrac = distance / SPREAD_RADIUS; float distanceMod = 1.0f - distanceFrac; float spreadChance = distanceMod; if (random() < spreadChance) { if (G_LineOfSight(entity.oldEnt, other.oldEnt) && other.Ignite(fireStarter)) { fireLogger.Notice("Ignited a neighbour, chance to do so was %.0f%%.", spreadChance*100.0f); } } }); // Don't spread again until re-ignited. spreadAt = INT_MAX; }
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; } }
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."); } } }
void ResourceStorageComponent::HandleDie(gentity_t* killer, meansOfDeath_t meansOfDeath) { // TODO: Add TeamComponent and/or Utility::Team. team_t team = entity.oldEnt->buildableTeam; float storedFraction = GetStoredFraction(); // Removes some of the owner's team's build points, proportional to the amount this structure // acquired and the amount of health lost (before deconstruction). float loss = (1.0f - entity.oldEnt->deconHealthFrac) * storedFraction * level.team[team].buildPoints * g_buildPointLossFraction.value; resourceStorageLogger.Notice( "A resource storage died, removing %.0f BP from the team's pool " "(%.1f × %.0f%% stored × %.0f%% health lost × %.0f total).", loss, g_buildPointLossFraction.value, 100.0f * storedFraction, 100.0f * (1.0f - entity.oldEnt->deconHealthFrac), level.team[team].buildPoints ); G_ModifyBuildPoints(team, -loss); // Main structures keep their account of acquired build points across lifecycles, it's saved // in a per-team variable and copied over to the ResourceStorageComponent whenever it's modified. if (!entity.Get<MainBuildableComponent>()) { G_ModifyTotalBuildPointsAcquired(team, -acquiredBuildPoints); } acquiredBuildPoints = 0.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 HealthComponent::HandleDamage(float amount, gentity_t* source, Util::optional<Vec3> location, Util::optional<Vec3> direction, int flags, meansOfDeath_t meansOfDeath) { if (health <= 0.0f) return; if (amount <= 0.0f) return; gclient_t *client = entity.oldEnt->client; // Check for immunity. if (entity.oldEnt->flags & FL_GODMODE) return; if (client) { if (client->noclip) return; if (client->sess.spectatorState != SPECTATOR_NOT) return; } // Set source to world if missing. if (!source) source = &g_entities[ENTITYNUM_WORLD]; // Don't handle ET_MOVER w/o die or pain function. // TODO: Handle mover special casing in a dedicated component. if (entity.oldEnt->s.eType == entityType_t::ET_MOVER && !(entity.oldEnt->die || entity.oldEnt->pain)) { // Special case for ET_MOVER with act function in initial position. if ((entity.oldEnt->moverState == MOVER_POS1 || entity.oldEnt->moverState == ROTATOR_POS1) && entity.oldEnt->act) { entity.oldEnt->act(entity.oldEnt, source, source); } return; } // Check for protection. if (!(flags & DAMAGE_NO_PROTECTION)) { // Check for protection from friendly damage. if (entity.oldEnt != source && G_OnSameTeam(entity.oldEnt, source)) { // Check if friendly fire has been disabled. if (!g_friendlyFire.integer) return; // Never do friendly damage on movement attacks. switch (meansOfDeath) { case MOD_LEVEL3_POUNCE: case MOD_LEVEL4_TRAMPLE: return; default: break; } // If dretchpunt is enabled and this is a dretch, do dretchpunt instead of damage. // TODO: Add a message for pushing. if (g_dretchPunt.integer && client && client->ps.stats[STAT_CLASS] == PCL_ALIEN_LEVEL0) { vec3_t dir, push; VectorSubtract(entity.oldEnt->r.currentOrigin, source->r.currentOrigin, dir); VectorNormalizeFast(dir); VectorScale(dir, (amount * 10.0f), push); push[ 2 ] = 64.0f; VectorAdd( client->ps.velocity, push, client->ps.velocity ); return; } } // Check for protection from friendly buildable damage. Never protect from building actions. // TODO: Use DAMAGE_NO_PROTECTION flag instead of listing means of death here. if (entity.oldEnt->s.eType == entityType_t::ET_BUILDABLE && source->client && meansOfDeath != MOD_DECONSTRUCT && meansOfDeath != MOD_SUICIDE && meansOfDeath != MOD_REPLACE) { if (G_OnSameTeam(entity.oldEnt, source) && !g_friendlyBuildableFire.integer) { return; } } } float take = amount; // Apply damage modifiers. if (!(flags & DAMAGE_PURE)) { entity.ApplyDamageModifier(take, location, direction, flags, meansOfDeath); } // Update combat timers. // TODO: Add a message to update combat timers. if (client && source->client && entity.oldEnt != source) { client->lastCombatTime = entity.oldEnt->client->lastCombatTime = level.time; } if (client) { // Save damage w/o armor modifier. client->damage_received += (int)(amount + 0.5f); // Save damage direction. if (direction) { VectorCopy(direction.value().Data(), client->damage_from); client->damage_fromWorld = false; } else { VectorCopy(entity.oldEnt->r.currentOrigin, client->damage_from); client->damage_fromWorld = true; } // Drain jetpack fuel. // TODO: Have another component handle jetpack fuel drain. client->ps.stats[STAT_FUEL] = std::max(0, client->ps.stats[STAT_FUEL] - (int)(amount + 0.5f) * JETPACK_FUEL_PER_DMG); // If boosted poison every attack. // TODO: Add a poison message and a PoisonableComponent. if (source->client && (source->client->ps.stats[STAT_STATE] & SS_BOOSTED) && client->pers.team == TEAM_HUMANS && client->poisonImmunityTime < level.time) { switch (meansOfDeath) { case MOD_POISON: case MOD_LEVEL2_ZAP: break; default: client->ps.stats[STAT_STATE] |= SS_POISONED; client->lastPoisonTime = level.time; client->lastPoisonClient = source; break; } } } healthLogger.Notice("Taking damage: %3.1f (%3.1f → %3.1f)", take, health, health - take); // Do the damage. health -= take; // Update team overlay info. if (client) client->pers.infoChangeTime = level.time; // TODO: Move lastDamageTime to HealthComponent. entity.oldEnt->lastDamageTime = level.time; // HACK: gentity_t.nextRegenTime only affects alien clients. // TODO: Catch damage message in a new RegenerationComponent. entity.oldEnt->nextRegenTime = level.time + ALIEN_CLIENT_REGEN_WAIT; // Handle non-self damage. if (entity.oldEnt != source) { float loss = take; if (health < 0.0f) loss += health; // TODO: Use ClientComponent. if (source->client) { // Add to the attacker's account on the target. // TODO: Move damage account array to HealthComponent. entity.oldEnt->credits[source->client->ps.clientNum].value += loss; entity.oldEnt->credits[source->client->ps.clientNum].time = level.time; entity.oldEnt->credits[source->client->ps.clientNum].team = (team_t)source->client->pers.team; } } // Handle death. // TODO: Send a Die/Pain message and handle details where appropriate. if (health <= 0) { healthLogger.Notice("Dying with %.1f health.", health); // Disable knockback. if (client) entity.oldEnt->flags |= FL_NO_KNOCKBACK; // Call legacy die function. if (entity.oldEnt->die) entity.oldEnt->die(entity.oldEnt, source, source, meansOfDeath); // Send die message. entity.Die(source, meansOfDeath); // Trigger ON_DIE event. if(!client) G_EventFireEntity(entity.oldEnt, source, ON_DIE); } else if (entity.oldEnt->pain) { entity.oldEnt->pain(entity.oldEnt, source, (int)std::ceil(take)); } if (entity.oldEnt != source && source->client) { bool lethal = (health <= 0); CombatFeedback::HitNotify(source, entity.oldEnt, location, take, meansOfDeath, lethal); } }