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; }
static void FirebombMissileThink( gentity_t *self ) { gentity_t *neighbor, *m; int subMissileNum; vec3_t dir, upwards = { 0.0f, 0.0f, 1.0f }; // ignite alien buildables in range neighbor = NULL; while ( ( neighbor = G_IterateEntitiesWithinRadius( neighbor, self->s.origin, FIREBOMB_IGNITE_RANGE ) ) ) { if ( neighbor->s.eType == ET_BUILDABLE && neighbor->buildableTeam == TEAM_ALIENS && G_LineOfSight( self, neighbor ) ) { G_IgniteBuildable( neighbor, self->parent ); } } // set floor below on fire (assumes the firebomb lays on the floor!) G_SpawnFire( self->s.origin, upwards, self->parent ); // spam fire for ( subMissileNum = 0; subMissileNum < FIREBOMB_SUBMISSILE_COUNT; subMissileNum++ ) { dir[ 0 ] = ( rand() / ( float )RAND_MAX ) - 0.5f; dir[ 1 ] = ( rand() / ( float )RAND_MAX ) - 0.5f; dir[ 2 ] = ( rand() / ( float )RAND_MAX ) * 0.5f; VectorNormalize( dir ); // the submissile's parent is the attacker m = G_SpawnMissile( MIS_FIREBOMB_SUB, self->parent, self->s.origin, dir, NULL, G_FreeEntity, level.time + 10000 ); // randomize missile speed VectorScale( m->s.pos.trDelta, ( rand() / ( float )RAND_MAX ) + 0.5f, m->s.pos.trDelta ); } // explode G_ExplodeMissile( self ); }
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 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; }