/** * @brief Attempts to refill jetpack fuel from a close source. * @return true if fuel was refilled. */ bool G_FindFuel( gentity_t *self ) { gentity_t *neighbor = nullptr; bool foundSource = false; if ( !self || !self->client ) { return false; } // search for fuel source while ( ( neighbor = G_IterateEntitiesWithinRadius( neighbor, self->s.origin, ENTITY_BUY_RANGE ) ) ) { // only friendly, living and powered buildables provide fuel if ( neighbor->s.eType != ET_BUILDABLE || !G_OnSameTeam( self, neighbor ) || !neighbor->spawned || !neighbor->powered || G_Dead( neighbor ) ) { continue; } switch ( neighbor->s.modelindex ) { case BA_H_ARMOURY: foundSource = true; break; } } if ( foundSource ) { return G_RefillFuel( self, true ); } return false; }
void G_WeightAttack( gentity_t *self, gentity_t *victim ) { float weightDPS; int attackerMass, victimMass, weightDamage; // weigth damage is only dealt between clients if ( !self->client || !victim->client ) { return; } // don't do friendly fire if ( G_OnSameTeam( self, victim ) ) { return; } // ignore invincible targets if ( !victim->takedamage ) { return; } // attacker must be above victim if ( self->client->ps.origin[ 2 ] + self->r.mins[ 2 ] < victim->s.origin[ 2 ] + victim->r.maxs[ 2 ] ) { return; } // victim must be on the ground if ( victim->client->ps.groundEntityNum == ENTITYNUM_NONE ) { return; } // check timer if ( victim->client->nextCrushTime > level.time ) { return; } attackerMass = BG_Class( self->client->pers.classSelection )->mass; victimMass = BG_Class( victim->client->pers.classSelection )->mass; weightDPS = WEIGHTDMG_DMG_MODIFIER * MAX( attackerMass - victimMass, 0 ); if ( weightDPS > WEIGHTDMG_DPS_THRESHOLD ) { weightDamage = ( int )( weightDPS * ( WEIGHTDMG_REPEAT / 1000.0f ) ); if ( weightDamage > 0 ) { G_Damage( victim, self, self, NULL, victim->s.origin, weightDamage, DAMAGE_NO_LOCDAMAGE, ModWeight( self ) ); } } victim->client->nextCrushTime = level.time + WEIGHTDMG_REPEAT; }
void G_ImpactAttack( gentity_t *self, gentity_t *victim ) { float impactVelocity, impactEnergy; vec3_t knockbackDir; int attackerMass, impactDamage; // self must be a client if ( !self->client ) { return; } // ignore invincible targets if ( !victim->takedamage ) { return; } // don't do friendly fire if ( G_OnSameTeam( self, victim ) ) { return; } // attacker must be above victim if ( self->client->ps.origin[ 2 ] + self->r.mins[ 2 ] < victim->s.origin[ 2 ] + victim->r.maxs[ 2 ] ) { return; } // allow the granger airlifting ritual if ( victim->client && victim->client->ps.stats[ STAT_STATE2 ] & SS2_JETPACK_ACTIVE && ( self->client->pers.classSelection == PCL_ALIEN_BUILDER0 || self->client->pers.classSelection == PCL_ALIEN_BUILDER0_UPG ) ) { return; } // calculate impact damage attackerMass = BG_Class( self->client->pers.classSelection )->mass; impactVelocity = fabs( self->client->pmext.fallImpactVelocity[ 2 ] ) * IMPACTDMG_QU_TO_METER; // in m/s impactEnergy = attackerMass * impactVelocity * impactVelocity; // in J impactDamage = ( int )( impactEnergy * IMPACTDMG_JOULE_TO_DAMAGE ); // deal impact damage to both clients and structures, use a threshold for friendly fire if ( impactDamage > 0 ) { // calculate knockback direction VectorSubtract( victim->s.origin, self->client->ps.origin, knockbackDir ); VectorNormalize( knockbackDir ); G_Damage( victim, self, self, knockbackDir, victim->s.origin, impactDamage, DAMAGE_NO_LOCDAMAGE, ModWeight( self ) ); } }
void G_WeightAttack( gentity_t *self, gentity_t *victim ) { float weightDPS, weightDamage; int attackerMass, victimMass; // weigth damage is only dealt between clients if ( !self->client || !victim->client ) { return; } // don't do friendly fire if ( G_OnSameTeam( self, victim ) ) { return; } // attacker must be above victim if ( self->client->ps.origin[ 2 ] + self->r.mins[ 2 ] < victim->s.origin[ 2 ] + victim->r.maxs[ 2 ] ) { return; } // victim must be on the ground if ( victim->client->ps.groundEntityNum == ENTITYNUM_NONE ) { return; } // check timer if ( victim->client->nextCrushTime > level.time ) { return; } attackerMass = BG_Class( self->client->pers.classSelection )->mass; victimMass = BG_Class( victim->client->pers.classSelection )->mass; weightDPS = WEIGHTDMG_DMG_MODIFIER * std::max( attackerMass - victimMass, 0 ); if ( weightDPS > WEIGHTDMG_DPS_THRESHOLD ) { weightDamage = weightDPS * ( WEIGHTDMG_REPEAT / 1000.0f ); victim->entity->Damage(weightDamage, self, Vec3::Load(victim->s.origin), Util::nullopt, DAMAGE_NO_LOCDAMAGE, ModWeight(self)); } victim->client->nextCrushTime = level.time + WEIGHTDMG_REPEAT; }
void G_ImpactAttack( gentity_t *self, gentity_t *victim ) { float impactVelocity, impactEnergy, impactDamage; vec3_t knockbackDir; int attackerMass; // self must be a client if ( !self->client ) { return; } // don't do friendly fire if ( G_OnSameTeam( self, victim ) ) { return; } // attacker must be above victim if ( self->client->ps.origin[ 2 ] + self->r.mins[ 2 ] < victim->s.origin[ 2 ] + victim->r.maxs[ 2 ] ) { return; } // allow the granger airlifting ritual if ( victim->client && victim->client->ps.stats[ STAT_STATE2 ] & SS2_JETPACK_ACTIVE && ( self->client->pers.classSelection == PCL_ALIEN_BUILDER0 || self->client->pers.classSelection == PCL_ALIEN_BUILDER0_UPG ) ) { return; } // calculate impact damage impactVelocity = fabs( self->client->pmext.fallImpactVelocity[ 2 ] ) * QU_TO_METER; // in m/s if (!impactVelocity) return; attackerMass = BG_Class( self->client->pers.classSelection )->mass; impactEnergy = attackerMass * impactVelocity * impactVelocity; // in J impactDamage = impactEnergy * IMPACTDMG_JOULE_TO_DAMAGE; // calculate knockback direction VectorSubtract( victim->s.origin, self->client->ps.origin, knockbackDir ); VectorNormalize( knockbackDir ); victim->entity->Damage((float)impactDamage, self, Vec3::Load(victim->s.origin), Vec3::Load(knockbackDir), DAMAGE_NO_LOCDAMAGE, ModWeight(self)); }
/** * @brief Attempts to refill ammo from a close source. * @return Whether ammo was refilled. */ qboolean G_FindAmmo( gentity_t *self ) { gentity_t *neighbor = NULL; qboolean foundSource = qfalse; // don't search for a source if refilling isn't possible if ( !CanUseAmmoRefill( self ) ) { return qfalse; } // search for ammo source while ( ( neighbor = G_IterateEntitiesWithinRadius( neighbor, self->s.origin, ENTITY_BUY_RANGE ) ) ) { // only friendly, living and powered buildables provide ammo if ( neighbor->s.eType != ET_BUILDABLE || !G_OnSameTeam( self, neighbor ) || !neighbor->spawned || !neighbor->powered || neighbor->health <= 0 ) { continue; } switch ( neighbor->s.modelindex ) { case BA_H_ARMOURY: foundSource = qtrue; break; case BA_H_REACTOR: case BA_H_REPEATER: if ( BG_Weapon( self->client->ps.stats[ STAT_WEAPON ] )->usesEnergy ) { foundSource = qtrue; } break; } } if ( foundSource ) { return G_RefillAmmo( self, qtrue ); } return qfalse; }
qboolean G_CheckVenomAttack( gentity_t *self ) { trace_t tr; gentity_t *traceEnt; int damage = LEVEL0_BITE_DMG; if ( self->client->ps.weaponTime ) { return qfalse; } // Calculate muzzle point AngleVectors( self->client->ps.viewangles, forward, right, up ); G_CalcMuzzlePoint( self, forward, right, up, muzzle ); G_WideTrace( &tr, self, LEVEL0_BITE_RANGE, LEVEL0_BITE_WIDTH, LEVEL0_BITE_WIDTH, &traceEnt ); if ( !traceEnt || !traceEnt->takedamage || traceEnt->health <= 0 || G_OnSameTeam( self, traceEnt ) ) { return qfalse; } // only allow bites to work against turrets or buildables in construction if ( traceEnt->s.eType == ET_BUILDABLE && traceEnt->spawned ) { switch ( traceEnt->s.modelindex ) { case BA_H_MGTURRET: case BA_H_TESLAGEN: break; default: return qfalse; } } SendMeleeHitEvent( self, traceEnt, &tr ); G_Damage( traceEnt, self, self, forward, tr.endpos, damage, DAMAGE_NO_KNOCKBACK, MOD_LEVEL0_BITE ); self->client->ps.weaponTime += LEVEL0_BITE_REPEAT; return qtrue; }
// 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; }
bool G_CheckVenomAttack( gentity_t *self ) { trace_t tr; gentity_t *traceEnt; int damage = LEVEL0_BITE_DMG; if ( self->client->ps.weaponTime ) { return false; } // Calculate muzzle point AngleVectors( self->client->ps.viewangles, forward, right, up ); G_CalcMuzzlePoint( self, forward, right, up, muzzle ); G_WideTrace( &tr, self, LEVEL0_BITE_RANGE, LEVEL0_BITE_WIDTH, LEVEL0_BITE_WIDTH, &traceEnt ); if ( !traceEnt || !traceEnt->takedamage || traceEnt->health <= 0 || G_OnSameTeam( self, traceEnt ) ) { return false; } // only allow bites to work against buildables in construction if ( traceEnt->s.eType == ET_BUILDABLE && traceEnt->spawned ) { return false; } SendMeleeHitEvent( self, traceEnt, &tr ); G_Damage( traceEnt, self, self, forward, tr.endpos, damage, 0, MOD_LEVEL0_BITE ); self->client->ps.weaponTime += LEVEL0_BITE_REPEAT; return true; }
bool G_CheckVenomAttack( gentity_t *self ) { trace_t tr; gentity_t *traceEnt; if ( self->client->ps.weaponTime ) { return false; } // Calculate muzzle point AngleVectors( self->client->ps.viewangles, forward, right, up ); G_CalcMuzzlePoint( self, forward, right, up, muzzle ); G_WideTrace( &tr, self, LEVEL0_BITE_RANGE, LEVEL0_BITE_WIDTH, LEVEL0_BITE_WIDTH, &traceEnt ); if ( !G_Alive( traceEnt ) || G_OnSameTeam( self, traceEnt ) ) { return false; } // only allow bites to work against buildables in construction if ( traceEnt->s.eType == ET_BUILDABLE && traceEnt->spawned ) { return false; } traceEnt->entity->Damage((float)LEVEL0_BITE_DMG, self, Vec3::Load(tr.endpos), Vec3::Load(forward), 0, (meansOfDeath_t)MOD_LEVEL0_BITE); SendMeleeHitEvent( self, traceEnt, &tr ); self->client->ps.weaponTime += LEVEL0_BITE_REPEAT; return true; }
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); } }
// TODO: Clean this mess further (split into helper functions) void G_Damage( gentity_t *target, gentity_t *inflictor, gentity_t *attacker, vec3_t dir, vec3_t point, int damage, int damageFlags, int mod ) { gclient_t *client; int take, loss; int knockback; float modifier; if ( !target || !target->takedamage || target->health <= 0 || level.intermissionQueued ) { return; } client = target->client; // don't handle noclip clients if ( client && client->noclip ) { return; } // set inflictor to world if missing if ( !inflictor ) { inflictor = &g_entities[ ENTITYNUM_WORLD ]; } // set attacker to world if missing if ( !attacker ) { attacker = &g_entities[ ENTITYNUM_WORLD ]; } // don't handle ET_MOVER w/o die or pain function if ( target->s.eType == ET_MOVER && !( target->die || target->pain ) ) { // special case for ET_MOVER with act function in initial position if ( ( target->moverState == MOVER_POS1 || target->moverState == ROTATOR_POS1 ) && target->act ) { target->act( target, inflictor, attacker ); } return; } // do knockback against clients if ( client && !( damageFlags & DAMAGE_NO_KNOCKBACK ) && dir ) { // scale knockback by weapon if ( inflictor->s.weapon != WP_NONE ) { knockback = ( int )( ( float )damage * BG_Weapon( inflictor->s.weapon )->knockbackScale ); } else { knockback = damage; } // apply generic damage to knockback modifier knockback *= DAMAGE_TO_KNOCKBACK; // HACK: Too much knockback from falling makes you bounce and looks silly if ( mod == MOD_FALLING ) { knockback = MIN( knockback, MAX_FALLDMG_KNOCKBACK ); } G_KnockbackByDir( target, dir, knockback, qfalse ); } else { // damage knockback gets saved, so initialize it here knockback = 0; } // godmode prevents damage if ( target->flags & FL_GODMODE ) { return; } // check for protection if ( !( damageFlags & DAMAGE_NO_PROTECTION ) ) { // check for protection from friendly damage if ( target != attacker && G_OnSameTeam( target, attacker ) ) { // check if friendly fire has been disabled if ( !g_friendlyFire.integer ) { return; } // don't do friendly damage on movement attacks switch ( mod ) { 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 if ( g_dretchPunt.integer && target->client && ( target->client->ps.stats[ STAT_CLASS ] == PCL_ALIEN_LEVEL0 || target->client->ps.stats[ STAT_CLASS ] == PCL_ALIEN_LEVEL0_UPG ) ) { vec3_t dir, push; VectorSubtract( target->r.currentOrigin, attacker->r.currentOrigin, dir ); VectorNormalizeFast( dir ); VectorScale( dir, ( damage * 10.0f ), push ); push[ 2 ] = 64.0f; VectorAdd( target->client->ps.velocity, push, target->client->ps.velocity ); return; } } // for buildables, never protect from damage dealt by building actions if ( target->s.eType == ET_BUILDABLE && attacker->client && mod != MOD_DECONSTRUCT && mod != MOD_SUICIDE && mod != MOD_REPLACE && mod != MOD_NOCREEP ) { // check for protection from friendly buildable damage if ( G_OnSameTeam( target, attacker ) && !g_friendlyBuildableFire.integer ) { return; } } } // update combat timers if ( target->client && attacker->client && target != attacker ) { target->client->lastCombatTime = level.time; attacker->client->lastCombatTime = level.time; } if ( client ) { // save damage (w/o armor modifier), knockback client->damage_received += damage; client->damage_knockback += knockback; // save damage direction if ( dir ) { VectorCopy( dir, client->damage_from ); client->damage_fromWorld = qfalse; } else { VectorCopy( target->r.currentOrigin, client->damage_from ); client->damage_fromWorld = qtrue; } // drain jetpack fuel client->ps.stats[ STAT_FUEL ] -= damage * JETPACK_FUEL_PER_DMG; if ( client->ps.stats[ STAT_FUEL ] < 0 ) { client->ps.stats[ STAT_FUEL ] = 0; } // apply damage modifier modifier = CalcDamageModifier( point, target, (class_t) client->ps.stats[ STAT_CLASS ], damageFlags ); take = ( int )( ( float )damage * modifier + 0.5f ); // if boosted poison every attack if ( attacker->client && ( attacker->client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) && target->client->pers.team == TEAM_HUMANS && target->client->poisonImmunityTime < level.time ) { switch ( mod ) { case MOD_POISON: case MOD_LEVEL1_PCLOUD: case MOD_LEVEL2_ZAP: break; default: target->client->ps.stats[ STAT_STATE ] |= SS_POISONED; target->client->lastPoisonTime = level.time; target->client->lastPoisonClient = attacker; } } } else { take = damage; } // make sure damage is done if ( take < 1 ) { take = 1; } if ( g_debugDamage.integer > 0 ) { G_Printf( "G_Damage: %3i (%3i → %3i)\n", take, target->health, target->health - take ); } // do the damage target->health = target->health - take; if ( target->client ) { target->client->ps.stats[ STAT_HEALTH ] = target->health; target->client->pers.infoChangeTime = level.time; // ? } target->lastDamageTime = level.time; // TODO: gentity_t->nextRegenTime only affects alien clients, remove it and use lastDamageTime // Optionally (if needed for some reason), move into client struct and add "Alien" to name target->nextRegenTime = level.time + ALIEN_CLIENT_REGEN_WAIT; // handle non-self damage if ( attacker != target ) { if ( target->health < 0 ) { loss = ( take + target->health ); } else { loss = take; } if ( attacker->client ) { // add to the attacker's account on the target target->credits[ attacker->client->ps.clientNum ] += ( float )loss; // notify the attacker of a hit NotifyClientOfHit( attacker ); } // update buildable stats if ( attacker->s.eType == ET_BUILDABLE && attacker->health > 0 ) { attacker->buildableStatsTotal += loss; } } // handle dying target if ( target->health <= 0 ) { // set no knockback flag for clients if ( client ) { target->flags |= FL_NO_KNOCKBACK; } // cap negative health if ( target->health < -999 ) { target->health = -999; } // call die function if ( target->die ) { target->die( target, inflictor, attacker, mod ); } // update buildable stats if ( attacker->s.eType == ET_BUILDABLE && attacker->health > 0 ) { attacker->buildableStatsCount++; } // for non-client victims, fire ON_DIE event if( !target->client ) { G_EventFireEntity( target, attacker, ON_DIE ); } return; } else if ( target->pain ) { target->pain( target, attacker, take ); } }
void G_PlayerDie( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int meansOfDeath ) { gentity_t *ent; int anim; int killer; int i; const char *killerName, *obit; if ( self->client->ps.pm_type == PM_DEAD ) { return; } if ( level.intermissiontime ) { return; } self->client->ps.pm_type = PM_DEAD; self->suicideTime = 0; if ( attacker ) { killer = attacker->s.number; if ( attacker->client ) { killerName = attacker->client->pers.netname; } else { killerName = "<world>"; } } else { killer = ENTITYNUM_WORLD; killerName = "<world>"; } if ( meansOfDeath < 0 || meansOfDeath >= ARRAY_LEN( modNames ) ) { // fall back on the number obit = va( "%d", meansOfDeath ); } else { obit = modNames[ meansOfDeath ]; } G_LogPrintf( "Die: %d %d %s: %s" S_COLOR_WHITE " killed %s\n", killer, ( int )( self - g_entities ), obit, killerName, self->client->pers.netname ); // deactivate all upgrades for ( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) { BG_DeactivateUpgrade( i, self->client->ps.stats ); } // broadcast the death event to everyone ent = G_NewTempEntity( self->r.currentOrigin, EV_OBITUARY ); ent->s.eventParm = meansOfDeath; ent->s.otherEntityNum = self->s.number; ent->s.otherEntityNum2 = killer; ent->r.svFlags = SVF_BROADCAST; // send to everyone if ( attacker && attacker->client ) { if ( ( attacker == self || G_OnSameTeam( self, attacker ) ) ) { //punish team kills and suicides if ( attacker->client->pers.team == TEAM_ALIENS ) { G_AddCreditToClient( attacker->client, -ALIEN_TK_SUICIDE_PENALTY, qtrue ); G_AddCreditsToScore( attacker, -ALIEN_TK_SUICIDE_PENALTY ); } else if ( attacker->client->pers.team == TEAM_HUMANS ) { G_AddCreditToClient( attacker->client, -HUMAN_TK_SUICIDE_PENALTY, qtrue ); G_AddCreditsToScore( attacker, -HUMAN_TK_SUICIDE_PENALTY ); } } else if ( g_showKillerHP.integer ) { trap_SendServerCommand( self - g_entities, va( "print_tr %s %s %3i", QQ( N_("Your killer, $1$^7, had $2$ HP.\n") ), Quote( killerName ), attacker->health ) ); } } else if ( attacker->s.eType != ET_BUILDABLE ) { if ( self->client->pers.team == TEAM_ALIENS ) { G_AddCreditsToScore( self, -ALIEN_TK_SUICIDE_PENALTY ); } else if ( self->client->pers.team == TEAM_HUMANS ) { G_AddCreditsToScore( self, -HUMAN_TK_SUICIDE_PENALTY ); } } // give credits for killing this player G_RewardAttackers( self ); ScoreboardMessage( self ); // show scores // send updated scores to any clients that are following this one, // or they would get stale scoreboards for ( i = 0; i < level.maxclients; i++ ) { gclient_t *client; client = &level.clients[ i ]; if ( client->pers.connected != CON_CONNECTED ) { continue; } if ( client->sess.spectatorState == SPECTATOR_NOT ) { continue; } if ( client->sess.spectatorClient == self->s.number ) { ScoreboardMessage( g_entities + i ); } } VectorCopy( self->s.origin, self->client->pers.lastDeathLocation ); self->takedamage = qfalse; // can still be gibbed self->s.weapon = WP_NONE; if ( self->client->noclip ) { self->client->cliprcontents = CONTENTS_CORPSE; } else { self->r.contents = CONTENTS_CORPSE; } self->s.angles[ PITCH ] = 0; self->s.angles[ ROLL ] = 0; self->s.angles[ YAW ] = self->s.apos.trBase[ YAW ]; LookAtKiller( self, inflictor, attacker ); VectorCopy( self->s.angles, self->client->ps.viewangles ); self->s.loopSound = 0; self->r.maxs[ 2 ] = -8; // don't allow respawn until the death anim is done // g_forcerespawn may force spawning at some later time self->client->respawnTime = level.time + 1700; // clear misc memset( self->client->ps.misc, 0, sizeof( self->client->ps.misc ) ); { static int i; if ( !( self->client->ps.persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) { switch ( i ) { case 0: anim = BOTH_DEATH1; break; case 1: anim = BOTH_DEATH2; break; case 2: default: anim = BOTH_DEATH3; break; } } else { switch ( i ) { case 0: anim = NSPA_DEATH1; break; case 1: anim = NSPA_DEATH2; break; case 2: default: anim = NSPA_DEATH3; break; } } self->client->ps.legsAnim = ( ( self->client->ps.legsAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | anim; if ( !( self->client->ps.persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) { self->client->ps.torsoAnim = ( ( self->client->ps.torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | anim; } // use own entityid if killed by non-client to prevent uint8_t overflow G_AddEvent( self, EV_DEATH1 + i, ( killer < MAX_CLIENTS ) ? killer : self - g_entities ); // globally cycle through the different death animations i = ( i + 1 ) % 3; } trap_LinkEntity( self ); self->client->pers.infoChangeTime = level.time; }
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; }