void G_ExplodeMissile( gentity_t *ent ) { vec3_t dir; vec3_t origin; const missileAttributes_t *ma = BG_Missile( ent->s.modelindex ); BG_EvaluateTrajectory( &ent->s.pos, level.time, origin ); SnapVector( origin ); G_SetOrigin( ent, origin ); // we don't have a valid direction, so just point straight up dir[ 0 ] = dir[ 1 ] = 0; dir[ 2 ] = 1; // turn the missile into an event carrier ent->s.eType = ET_INVISIBLE; ent->freeAfterEvent = true; G_AddEvent( ent, EV_MISSILE_HIT_ENVIRONMENT, DirToByte( dir ) ); // splash damage if ( ent->splashDamage ) { G_RadiusDamage( ent->r.currentOrigin, ent->parent, ent->splashDamage * MissileTimeSplashDmgMod( ent ), ent->splashRadius, ent, ( ma->doKnockback ? DAMAGE_KNOCKBACK : 0 ), ent->splashMethodOfDeath ); } trap_LinkEntity( ent ); }
static void RocketThink( gentity_t *self ) { vec3_t currentDir, targetDir, newDir, rotAxis; float rotAngle; if ( level.time > self->timestamp ) { self->think = G_ExplodeMissile; self->nextthink = level.time; return; } self->nextthink = level.time + ROCKET_TURN_PERIOD; // Calculate current and target direction. VectorNormalize2( self->s.pos.trDelta, currentDir ); VectorSubtract( self->target->r.currentOrigin, self->r.currentOrigin, targetDir ); VectorNormalize( targetDir ); // Don't turn anymore after the target was passed. if ( DotProduct( currentDir, targetDir ) < 0 ) { return; } // Calculate new direction. Use a fixed turning angle. CrossProduct( currentDir, targetDir, rotAxis ); rotAngle = RAD2DEG( acos( DotProduct( currentDir, targetDir ) ) ); RotatePointAroundVector( newDir, rotAxis, currentDir, Math::Clamp( rotAngle, -ROCKET_TURN_ANGLE, ROCKET_TURN_ANGLE ) ); // Check if new direction is safe. Turn anyway if old direction is unsafe, too. if ( !G_RocketpodSafeShot( ENTITYNUM_NONE, self->r.currentOrigin, newDir ) && G_RocketpodSafeShot( ENTITYNUM_NONE, self->r.currentOrigin, currentDir ) ) { return; } // Update trajectory. VectorScale( newDir, BG_Missile( self->s.modelindex )->speed, self->s.pos.trDelta ); SnapVector( self->s.pos.trDelta ); VectorCopy( self->r.currentOrigin, self->s.pos.trBase ); // TODO: Snap this, too? self->s.pos.trTime = level.time; }
bool G_RocketpodSafeShot( int passEntityNum, vec3_t origin, vec3_t dir ) { trace_t tr; vec3_t mins, maxs, end; float size; const missileAttributes_t *attr = BG_Missile( MIS_ROCKET ); size = attr->size; VectorSet( mins, -size, -size, -size); VectorSet( maxs, size, size, size ); VectorMA( origin, 8192, dir, end ); trap_Trace( &tr, origin, mins, maxs, end, passEntityNum, MASK_SHOT, 0 ); return !G_RadiusDamage( tr.endpos, nullptr, attr->splashDamage, attr->splashRadius, nullptr, 0, MOD_ROCKETPOD, TEAM_HUMANS ); }
// TODO: Use a proper trajectory trace, once available, to ensure friendly buildables are never hit. bool SpikerComponent::SafeToShoot(Vec3 direction) { const missileAttributes_t* ma = BG_Missile(MIS_SPIKER); float missileSize = (float)ma->size; trace_t trace; vec3_t mins, maxs; Vec3 end = Vec3::Load(entity.oldEnt->s.origin) + (SPIKE_RANGE * direction); // Test once with normal and once with inflated missile bounding box. for (float traceSize : {missileSize, missileSize * SAFETY_TRACE_INFLATION}) { mins[0] = mins[1] = mins[2] = -traceSize; maxs[0] = maxs[1] = maxs[2] = traceSize; trap_Trace(&trace, entity.oldEnt->s.origin, mins, maxs, end.Data(), entity.oldEnt->s.number, ma->clipmask, 0); gentity_t* hit = &g_entities[trace.entityNum]; if (hit && G_OnSameTeam(entity.oldEnt, hit)) { return false; } } return true; }
gentity_t *G_SpawnMissile( missile_t missile, gentity_t *parent, vec3_t start, vec3_t dir, gentity_t *target, void ( *think )( gentity_t *self ), int nextthink ) { gentity_t *m; const missileAttributes_t *ma; vec3_t velocity; if ( !parent ) { return nullptr; } ma = BG_Missile( missile ); m = G_NewEntity(); // generic m->s.eType = ET_MISSILE; m->s.modelindex = missile; m->r.ownerNum = parent->s.number; m->parent = parent; m->target = target; m->think = think; m->nextthink = nextthink; // from attribute config file m->s.weapon = ma->number; m->classname = ma->name; m->pointAgainstWorld = ma->pointAgainstWorld; m->damage = ma->damage; m->methodOfDeath = ma->meansOfDeath; m->splashDamage = ma->splashDamage; m->splashRadius = ma->splashRadius; m->splashMethodOfDeath = ma->splashMeansOfDeath; m->clipmask = ma->clipmask; m->r.mins[ 0 ] = m->r.mins[ 1 ] = m->r.mins[ 2 ] = -ma->size; m->r.maxs[ 0 ] = m->r.maxs[ 1 ] = m->r.maxs[ 2 ] = ma->size; m->s.eFlags = ma->flags; // not yet implemented / deprecated m->flightSplashDamage = 0; m->flightSplashRadius = 0; // trajectory { // set trajectory type m->s.pos.trType = ma->trajectoryType; // move a bit on the first frame m->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // set starting point VectorCopy( start, m->s.pos.trBase ); VectorCopy( start, m->r.currentOrigin ); // set speed VectorScale( dir, ma->speed, velocity ); // add lag if ( ma->lag && parent->client ) { VectorMA( velocity, ma->lag, parent->client->ps.velocity, velocity ); } // copy velocity VectorCopy( velocity, m->s.pos.trDelta ); // save net bandwidth SnapVector( m->s.pos.trDelta ); } return m; }
static void MissileImpact( gentity_t *ent, trace_t *trace ) { int dirAsByte, impactFlags; const missileAttributes_t *ma = BG_Missile( ent->s.modelindex ); gentity_t *hitEnt = &g_entities[ trace->entityNum ]; gentity_t *attacker = &g_entities[ ent->r.ownerNum ]; // Returns whether damage and hit effects should be done and played. std::function<int(gentity_t*, trace_t*, gentity_t*)> impactFunc; // Check for bounce. if ( ent->s.eFlags & ( EF_BOUNCE | EF_BOUNCE_HALF ) && !HasComponents<HealthComponent>(*hitEnt->entity) ) { BounceMissile( ent, trace ); if ( !( ent->s.eFlags & EF_NO_BOUNCE_SOUND ) ) { G_AddEvent( ent, EV_GRENADE_BOUNCE, 0 ); } return; } // Call missile specific impact functions. switch( ent->s.modelindex ) { case MIS_GRENADE: impactFunc = ImpactGrenade; break; case MIS_FIREBOMB: impactFunc = ImpactGrenade; break; case MIS_FLAMER: impactFunc = ImpactFlamer; break; case MIS_FIREBOMB_SUB: impactFunc = ImpactFirebombSub; break; case MIS_LOCKBLOB: impactFunc = ImpactLockblock; break; case MIS_SLOWBLOB: impactFunc = ImpactSlowblob; break; case MIS_HIVE: impactFunc = ImpactHive; break; default: impactFunc = DefaultImpactFunc; break; } impactFlags = impactFunc( ent, trace, hitEnt ); // Deal impact damage. if ( !( impactFlags & MIF_NO_DAMAGE ) ) { if ( ent->damage && G_Alive( hitEnt ) ) { vec3_t dir; BG_EvaluateTrajectoryDelta( &ent->s.pos, level.time, dir ); if ( VectorNormalize( dir ) == 0 ) { dir[ 2 ] = 1; // stepped on a grenade } int dflags = 0; if ( !ma->doLocationalDamage ) dflags |= DAMAGE_NO_LOCDAMAGE; if ( ma->doKnockback ) dflags |= DAMAGE_KNOCKBACK; hitEnt->entity->Damage(ent->damage * MissileTimeDmgMod(ent), attacker, Vec3::Load(trace->endpos), Vec3::Load(dir), dflags, (meansOfDeath_t)ent->methodOfDeath); } // splash damage (doesn't apply to person directly hit) if ( ent->splashDamage ) { G_RadiusDamage( trace->endpos, ent->parent, ent->splashDamage * MissileTimeSplashDmgMod( ent ), ent->splashRadius, hitEnt, ( ma->doKnockback ? DAMAGE_KNOCKBACK : 0 ), ent->splashMethodOfDeath ); } } // Play hit effects and remove the missile. if ( !( impactFlags & MIF_NO_EFFECT ) ) { // Use either the trajectory direction or the surface normal for the hit event. if ( ma->impactFlightDirection ) { vec3_t trajDir; BG_EvaluateTrajectoryDelta( &ent->s.pos, level.time, trajDir ); VectorNormalize( trajDir ); dirAsByte = DirToByte( trajDir ); } else { dirAsByte = DirToByte( trace->plane.normal ); } // Add hit event. if ( HasComponents<HealthComponent>(*hitEnt->entity) ) { G_AddEvent( ent, EV_MISSILE_HIT_ENTITY, dirAsByte ); ent->s.otherEntityNum = hitEnt->s.number; } else if ( trace->surfaceFlags & SURF_METAL ) { G_AddEvent( ent, EV_MISSILE_HIT_METAL, dirAsByte ); } else { G_AddEvent( ent, EV_MISSILE_HIT_ENVIRONMENT, dirAsByte ); } ent->freeAfterEvent = true; // HACK: Change over to a general entity at the point of impact. ent->s.eType = ET_GENERAL; // Prevent map models from appearing at impact point. ent->s.modelindex = 0; // Save net bandwith. G_SnapVectorTowards( trace->endpos, ent->s.pos.trBase ); G_SetOrigin( ent, trace->endpos ); trap_LinkEntity( ent ); } // If no impact happened, check if we should continue or free ourselves. else if ( !( impactFlags & MIF_NO_FREE ) ) { G_FreeEntity( ent ); } }
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; }