예제 #1
0
void ctrl_relay_act( gentity_t *self, gentity_t*, gentity_t *activator )
{
	if (!self->enabled)
		return;

	if ( !self->config.wait.time )
	{
		G_EventFireEntity( self, activator, ON_ACT );
	}
	else
	{
		self->nextthink = VariatedLevelTime( self->config.wait );
		self->think = think_fireOnActDelayed;
		self->activator = activator;
	}
}
예제 #2
0
void G_CallEntity(gentity_t *targetedEntity, gentityCall_t *call)
{
	if ( g_debugEntities.integer > 1 )
	{
		G_Printf(S_DEBUG "[%s] %s calling %s %s:%s\n",
				etos( call->activator ),
				etos( call->caller ),
				call->definition ? call->definition->event : "onUnknown",
				etos( targetedEntity ),
				call->definition && call->definition->action ? call->definition->action : "default");
	}

	targetedEntity->callIn = *call;

	if((!targetedEntity->handleCall || !targetedEntity->handleCall(targetedEntity, call)) && call->definition)
	{
		switch (call->definition->actionType)
		{
		case ECA_NOP:
			break;

		case ECA_CUSTOM:
			if ( g_debugEntities.integer > -1 )
			{
				G_Printf(S_WARNING "Unknown action \"%s\" for %s\n",
						call->definition->action, etos(targetedEntity));
			}
			break;

		case ECA_FREE:
			G_FreeEntity(targetedEntity);
			return; //we have to handle notification differently in the free-case

		case ECA_PROPAGATE:
			G_FireEntity( targetedEntity, call->activator);
			break;

		case ECA_ENABLE:
			if(!targetedEntity->enabled) //only fire an event if we weren't already enabled
			{
				targetedEntity->enabled = true;
				G_EventFireEntity( targetedEntity, call->activator, ON_ENABLE );
			}
			break;
		case ECA_DISABLE:
			if(targetedEntity->enabled) //only fire an event if we weren't already disabled
			{
				targetedEntity->enabled = false;
				G_EventFireEntity( targetedEntity, call->activator, ON_DISABLE );
			}
			break;
		case ECA_TOGGLE:
			targetedEntity->enabled = !targetedEntity->enabled;
			G_EventFireEntity( targetedEntity, call->activator, targetedEntity->enabled ? ON_ENABLE : ON_DISABLE );
			break;

		case ECA_USE:
			if (!targetedEntity->use)
			{
				if(g_debugEntities.integer >= 0)
					G_Printf(S_WARNING "calling :use on %s, which has no use function!\n", etos(targetedEntity));
				break;
			}
			if(!call->activator || !call->activator->client)
			{
				if(g_debugEntities.integer >= 0)
					G_Printf(S_WARNING "calling %s:use, without a client as activator.\n", etos(targetedEntity));
				break;
			}
			targetedEntity->use(targetedEntity, call->caller, call->activator);
			break;
		case ECA_RESET:
			if (targetedEntity->reset)
			{
				targetedEntity->reset(targetedEntity);
				G_EventFireEntity( targetedEntity, call->activator, ON_RESET );
			}
			break;
		case ECA_ACT:
			G_HandleActCall( targetedEntity, call );
			break;

		default:
			if (targetedEntity->act)
				targetedEntity->act(targetedEntity, call->caller, call->activator);
			break;
		}
	}
	if(targetedEntity->notifyHandler)
		targetedEntity->notifyHandler( targetedEntity, call );

	targetedEntity->callIn = NULL_CALL; /**< not called anymore */
}
예제 #3
0
void G_FireEntity( gentity_t *self, gentity_t *activator )
{
	G_EventFireEntity( self, activator, ON_DEFAULT );
}
예제 #4
0
void think_fireOnActDelayed( gentity_t *self )
{
	G_EventFireEntity( self, self->activator, ON_ACT );
}
예제 #5
0
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);
	}
}
예제 #6
0
// 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 );
	}
}
예제 #7
0
/*
===========
ClientSpawn

Called every time a client is placed fresh in the world:
after the first ClientBegin, and after each respawn and evolve
Initializes all non-persistent parts of playerState
============
*/
void ClientSpawn( gentity_t *ent, gentity_t *spawn, const vec3_t origin, const vec3_t angles )
{
	int                index;
	vec3_t             spawn_origin, spawn_angles;
	gclient_t          *client;
	int                i;
	clientPersistant_t saved;
	clientSession_t    savedSess;
	bool           savedNoclip, savedCliprcontents;
	int                persistant[ MAX_PERSISTANT ];
	gentity_t          *spawnPoint = nullptr;
	int                flags;
	int                savedPing;
	int                teamLocal;
	int                eventSequence;
	char               userinfo[ MAX_INFO_STRING ];
	vec3_t             up = { 0.0f, 0.0f, 1.0f };
	int                maxAmmo, maxClips;
	weapon_t           weapon;

	ClientSpawnCBSE(ent, ent == spawn);

	index = ent - g_entities;
	client = ent->client;

	teamLocal = client->pers.team;

	//if client is dead and following teammate, stop following before spawning
	if ( client->sess.spectatorClient != -1 )
	{
		client->sess.spectatorClient = -1;
		client->sess.spectatorState = SPECTATOR_FREE;
	}

	// only start client if chosen a class and joined a team
	if ( client->pers.classSelection == PCL_NONE && teamLocal == TEAM_NONE )
	{
		client->sess.spectatorState = SPECTATOR_FREE;
	}
	else if ( client->pers.classSelection == PCL_NONE )
	{
		client->sess.spectatorState = SPECTATOR_LOCKED;
	}

	// if client is dead and following teammate, stop following before spawning
	if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW )
	{
		G_StopFollowing( ent );
	}

	if ( origin != nullptr )
	{
		VectorCopy( origin, spawn_origin );
	}

	if ( angles != nullptr )
	{
		VectorCopy( angles, spawn_angles );
	}

	// find a spawn point
	// do it before setting health back up, so farthest
	// ranging doesn't count this client
	if ( client->sess.spectatorState != SPECTATOR_NOT )
	{
		if ( teamLocal == TEAM_NONE )
		{
			spawnPoint = G_SelectSpectatorSpawnPoint( spawn_origin, spawn_angles );
		}
		else if ( teamLocal == TEAM_ALIENS )
		{
			spawnPoint = G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles );
		}
		else if ( teamLocal == TEAM_HUMANS )
		{
			spawnPoint = G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles );
		}
	}
	else
	{
		if ( spawn == nullptr )
		{
			Com_Error(errorParm_t::ERR_DROP,  "ClientSpawn: spawn is NULL" );
		}

		spawnPoint = spawn;

		if ( spawnPoint->s.eType == entityType_t::ET_BUILDABLE )
		{
			G_SetBuildableAnim( spawnPoint, BANIM_SPAWN1, true );

			if ( spawnPoint->buildableTeam == TEAM_ALIENS )
			{
				spawnPoint->clientSpawnTime = ALIEN_SPAWN_REPEAT_TIME;
			}
			else if ( spawnPoint->buildableTeam == TEAM_HUMANS )
			{
				spawnPoint->clientSpawnTime = HUMAN_SPAWN_REPEAT_TIME;
			}
		}
	}

	// toggle the teleport bit so the client knows to not lerp
	flags = ( ent->client->ps.eFlags & EF_TELEPORT_BIT ) ^ EF_TELEPORT_BIT;
	G_UnlaggedClear( ent );

	// clear everything but the persistent data

	saved = client->pers;
	savedSess = client->sess;
	savedPing = client->ps.ping;
	savedNoclip = client->noclip;
	savedCliprcontents = client->cliprcontents;

	for ( i = 0; i < MAX_PERSISTANT; i++ )
	{
		persistant[ i ] = client->ps.persistant[ i ];
	}

	eventSequence = client->ps.eventSequence;
	memset( client, 0, sizeof( *client ) );

	client->pers = saved;
	client->sess = savedSess;
	client->ps.ping = savedPing;
	client->noclip = savedNoclip;
	client->cliprcontents = savedCliprcontents;

	for ( i = 0; i < MAX_PERSISTANT; i++ )
	{
		client->ps.persistant[ i ] = persistant[ i ];
	}

	client->ps.eventSequence = eventSequence;

	// increment the spawncount so the client will detect the respawn
	client->ps.persistant[ PERS_SPAWN_COUNT ]++;
	client->ps.persistant[ PERS_SPECSTATE ] = client->sess.spectatorState;

	client->airOutTime = level.time + 12000;

	trap_GetUserinfo( index, userinfo, sizeof( userinfo ) );
	client->ps.eFlags = flags;

	//Log::Notice( "ent->client->pers->pclass = %i\n", ent->client->pers.classSelection );

	ent->s.groundEntityNum = ENTITYNUM_NONE;
	ent->client = &level.clients[ index ];
	ent->classname = S_PLAYER_CLASSNAME;
	if ( client->noclip )
	{
		client->cliprcontents = CONTENTS_BODY;
	}
	else
	{
		ent->r.contents = CONTENTS_BODY;
	}
	ent->clipmask = MASK_PLAYERSOLID;
	ent->die = G_PlayerDie;
	ent->waterlevel = 0;
	ent->watertype = 0;
	ent->flags &= FL_GODMODE | FL_NOTARGET;

	// calculate each client's acceleration
	ent->evaluateAcceleration = true;

	client->ps.stats[ STAT_MISC ] = 0;

	client->ps.eFlags = flags;
	client->ps.clientNum = index;

	BG_ClassBoundingBox( ent->client->pers.classSelection, ent->r.mins, ent->r.maxs, nullptr, nullptr, nullptr );

	// clear entity values
	if ( ent->client->pers.classSelection == PCL_HUMAN_NAKED )
	{
		BG_AddUpgradeToInventory( UP_MEDKIT, client->ps.stats );
		weapon = client->pers.humanItemSelection;
	}
	else if ( client->sess.spectatorState == SPECTATOR_NOT )
	{
		weapon = BG_Class( ent->client->pers.classSelection )->startWeapon;
	}
	else
	{
		weapon = WP_NONE;
	}

	maxAmmo = BG_Weapon( weapon )->maxAmmo;
	maxClips = BG_Weapon( weapon )->maxClips;
	client->ps.stats[ STAT_WEAPON ] = weapon;
	client->ps.ammo = maxAmmo;
	client->ps.clips = maxClips;

	// We just spawned, not changing weapons
	client->ps.persistant[ PERS_NEWWEAPON ] = 0;

	client->ps.persistant[ PERS_TEAM ] = client->pers.team;

	// TODO: Check whether stats can be cleared at once instead of per field
	client->ps.stats[ STAT_STAMINA ] = STAMINA_MAX;
	client->ps.stats[ STAT_FUEL ]    = JETPACK_FUEL_MAX;
	client->ps.stats[ STAT_CLASS ] = ent->client->pers.classSelection;
	client->ps.stats[ STAT_BUILDABLE ] = BA_NONE;
	client->ps.stats[ STAT_PREDICTION ] = 0;
	client->ps.stats[ STAT_STATE ] = 0;

	VectorSet( client->ps.grapplePoint, 0.0f, 0.0f, 1.0f );

	//clear the credits array
	// TODO: Handle in HealthComponent or ClientComponent.
	for ( i = 0; i < MAX_CLIENTS; i++ )
	{
		ent->credits[ i ].value = 0.0f;
		ent->credits[ i ].time = 0;
		ent->credits[ i ].team = TEAM_NONE;
	}

	G_SetOrigin( ent, spawn_origin );
	VectorCopy( spawn_origin, client->ps.origin );

	//give aliens some spawn velocity
	if ( client->sess.spectatorState == SPECTATOR_NOT &&
	     client->pers.team == TEAM_ALIENS )
	{
		if ( ent == spawn )
		{
			//evolution particle system
			G_AddPredictableEvent( ent, EV_ALIEN_EVOLVE, DirToByte( up ) );
		}
		else
		{
			spawn_angles[ YAW ] += 180.0f;
			AngleNormalize360( spawn_angles[ YAW ] );

			if ( spawnPoint->s.origin2[ 2 ] > 0.0f )
			{
				vec3_t forward, dir;

				AngleVectors( spawn_angles, forward, nullptr, nullptr );
				VectorAdd( spawnPoint->s.origin2, forward, dir );
				VectorNormalize( dir );
				VectorScale( dir, BG_Class( ent->client->pers.classSelection )->jumpMagnitude,
				             client->ps.velocity );
			}

			G_AddPredictableEvent( ent, EV_PLAYER_RESPAWN, 0 );
		}
	}
	else if ( client->sess.spectatorState == SPECTATOR_NOT &&
	          client->pers.team == TEAM_HUMANS )
	{
		spawn_angles[ YAW ] += 180.0f;
		AngleNormalize360( spawn_angles[ YAW ] );
	}

	// the respawned flag will be cleared after the attack and jump keys come up
	client->ps.pm_flags |= PMF_RESPAWNED;

	trap_GetUsercmd( client - level.clients, &ent->client->pers.cmd );
	G_SetClientViewAngle( ent, spawn_angles );

	if ( client->sess.spectatorState == SPECTATOR_NOT )
	{
		trap_LinkEntity( ent );

		// force the base weapon up
		if ( client->pers.team == TEAM_HUMANS )
		{
			G_ForceWeaponChange( ent, weapon );
		}

		client->ps.weaponstate = WEAPON_READY;
	}

	// don't allow full run speed for a bit
	client->ps.pm_flags |= PMF_TIME_KNOCKBACK;
	client->ps.pm_time = 100;

	client->respawnTime = level.time;
	ent->nextRegenTime = level.time;

	client->inactivityTime = level.time + g_inactivity.integer * 1000;
	usercmdClearButtons( client->latched_buttons );

	// set default animations
	client->ps.torsoAnim = TORSO_STAND;
	client->ps.legsAnim = LEGS_IDLE;

	if ( level.intermissiontime )
	{
		MoveClientToIntermission( ent );
	}
	else
	{
		// fire the targets of the spawn point
		if ( !spawn && spawnPoint )
		{
			G_EventFireEntity( spawnPoint, ent, ON_SPAWN );
		}

		// select the highest weapon number available, after any
		// spawn given items have fired
		client->ps.weapon = 1;

		for ( i = WP_NUM_WEAPONS - 1; i > 0; i-- )
		{
			if ( BG_InventoryContainsWeapon( i, client->ps.stats ) )
			{
				client->ps.weapon = i;
				break;
			}
		}
	}

	// run a client frame to drop exactly to the floor,
	// initialize animations and other things
	client->ps.commandTime = level.time - 100;
	ent->client->pers.cmd.serverTime = level.time;
	ClientThink( ent - g_entities );

	// positively link the client, even if the command times are weird
	if ( client->sess.spectatorState == SPECTATOR_NOT )
	{
		BG_PlayerStateToEntityState( &client->ps, &ent->s, true );
		VectorCopy( ent->client->ps.origin, ent->r.currentOrigin );
		trap_LinkEntity( ent );
	}

	// must do this here so the number of active clients is calculated
	CalculateRanks();

	// run the presend to set anything else
	ClientEndFrame( ent );

	// clear entity state values
	BG_PlayerStateToEntityState( &client->ps, &ent->s, true );

	client->pers.infoChangeTime = level.time;

	// (re)tag the client for its team
	Beacon::DeleteTags( ent );
	Beacon::Tag( ent, (team_t)ent->client->ps.persistant[ PERS_TEAM ], true );
}