Quake Style - Quake 3 Tutorials
Freeze Tag - Freeze Tag Part 1
Starting off the freeze tag tuts..

Making a Player Freeze

The first thing I worked on for Freeze was getting the player to actually freeze. The plan is to make it so they can fly around when dead. This means we'll have to leave a body behind in the spot they died. Here's g_combat.c player_die


	self->r.maxs[2] = -8;

	// don't allow respawn until the death anim is done
	// g_forcerespawn may force spawning at some later time

/*freeze
	self->client->respawnTime = level.time + 1700;
freeze*/


	// remove powerups
	memset( self->client->ps.powerups, 0, sizeof(self->client->ps.powerups) );

//freeze
	player_freeze( self, attacker, meansOfDeath );
	self->client->respawnTime = level.time + 1700;
//freeze

	// never gib in a nodrop
	if ( self->health <= GIB_HEALTH && !(contents & CONTENTS_NODROP) && g_blood.integer ) {
		// gib death
		GibEntity( self, killer );
	} else {
		// normal death
		static int i;

I call player_freeze BEFORE it checks to GibEntity from excessive damage. This way if we are freezing them I can set their health to GIB_HEALTH + 1. Here's g_freeze.c player_freeze

 

void player_freeze( gentity_t *targ, gentity_t *attacker, int mod ) 
{
// Don't freeze anyone if we're not in team play or during warmup time
	if ( g_gametype.integer < GT_TEAM || level.warmupTime ) {
		return;
	}
// If they just (3 sec) respawned then don't freeze them
// Note we check their respawnTime so earlier I made sure this didn't get reset until after player_freeze was called
	if ( level.time - targ->client->respawnTime < 3000 ) {
		return;
	}
// Don't freeze someone for certain cases
	switch ( mod ) {
	case MOD_WATER:
	case MOD_CRUSH:
	case MOD_TELEFRAG:
	case MOD_FALLING:
	case MOD_SUICIDE:
	case MOD_TARGET_LASER:
	case MOD_TRIGGER_HURT:
	case MOD_GRAPPLE:
		return;
	}
// Did a teammate kill us or did we kill ourselves?
	if ( OnSameTeam( targ, attacker ) && targ != attacker ) {
		return;
	}

	targ->client->ps.powerups[ PW_BALL ] = INT_MAX;
	targ->client->noclip = qtrue;
	targ->health = 0;
	G_AddEvent( targ, EV_DEATH1, 0 );
	targ->client->ps.pm_time = 4000;
}


The last lines are very important. Let me show you this g_freeze.c IsFrozen

 

qboolean IsFrozen( gentity_t *ent ) {
// For clients and bots
	if ( ent->client && ent->client->ps.powerups[ PW_BALL ] ) {
		return qtrue;
// For my fake bodies
	} else if ( ent->s.powerups & ( 1 << PW_BALL ) ) {
		return qtrue;
	}
	return qfalse;
}
 

So the only way I know someone is frozen is if they have the powerup PW_BALL. THIS IS VERY IMPORTANT! The game (server) talks to cgame (client). How do I tell the cgame someone is frozen so that it can render them with a frozen look? Only certain stuff is "passed" to cgame and powerups are one of them. I noticed PW_BALL wasn't being used so I use that. This way I don't add any extra variable to the network code. Then I make the player noclip so they can move around, set their health to 0 so they don't GibEntity and that pm_time is there to hold them in place for 4 seconds so they can recognize they just died. ANOTHER VERY IMPORTANT TRICK I did was use G_AddEvent and EV_DEATH1. Let's look at cgame cg_event.c CG_EntityEvent

 
	case EV_DEATH1:

//freeze
		if ( es->number == cg.snap->ps.clientNum ) {
			cg.powerupTime = cg.time;
		}
		break;
//freeze
 
	case EV_DEATH2:
	case EV_DEATH3:
		DEBUGNAME("EV_DEATHx");
		trap_S_StartSound( NULL, es->number, CHAN_VOICE, 
				CG_CustomSound( es->number, va("*death%i.wav", event - EV_DEATH1 + 1) ) );
		break;

EV_DEATH1 is never called by anything. player_die calls EV_DEATH1 + 1 which is just EV_DEATH2. So EV_DEATH1 is like a free event I can use (kinda like PW_BALL was free to change). I didn't want to bother with worrying about adding any new event. What this does is, this event is sent to every client. If you happen to be THE player that just died then we will set your powerupTime to the current time. Look throughout the source and you'll see powerupTime is free to use. I do this a lot and it's probably a bad practice, but throughout Freeze Tag I take other variables and use them to my whim. I made sure nothing else is using them during the time I use them. Anyways, what this will do is call up the scoreboard for our player that just frozen since the scoreboard isn't normally displayed if you're in noclip mode. Here's cg_scoreboard.c CG_DrawScoreboard
 
	if ( cg.showScores || cg.predictedPlayerState.pm_type == PM_DEAD ||

//freeze
		( cg.predictedPlayerState.pm_type == PM_NOCLIP && cg.powerupTime + 4000 > cg.time ) ||
//freeze

		 cg.predictedPlayerState.pm_type == PM_INTERMISSION ) {
		fade = 1.0;
		fadeColor = colorWhite;
	} 
else {

It'll show the scores if you are pressing TAB (cg.showScores), if you're PM_DEAD, or if you're PM_NOCLIP (our frozen friend) and your powerupTime is still 4 seconds close to current time.
Now our player's dead so back in g_combat.c player_die
 
		self->client->ps.torsoAnim = 
			( ( self->client->ps.torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | anim;

//freeze
		if ( IsFrozen( self ) ) {
			CopyToBody( self );
		} else {
//freeze

		G_AddEvent( self, EV_DEATH1 + 1, killer );

//freeze
		}
//freeze

		// the body can still be gibbed
		self->die = body_die;

We're going to make a body for them (CopyToBody) if they're frozen, otherwise we'll make a screaming death noise. Here's g_freeze.c CopyToBody



void CopyToBody( gentity_t *ent ) {
	gentity_t	*body;

// Create a game entity (our lovely frozen body)
	body = G_Spawn();
	if ( !body ) {
		return;
	}

	body->classname = "body";
// This is basically a copy of g_client.c CopyToBodyQue (normal body created when dead player respawns)
	body->s = ent->s;
	body->s.eFlags = EF_DEAD;
// This body's frozen
	body->s.powerups = ( 1 << PW_BALL );
	body->s.loopSound = 0;
	body->s.number = body - g_entities;
	body->timestamp = level.time;
	body->physicsObject = qtrue;
	body->physicsBounce = 0;

// This is special
// This means we can shoot and bounce this body around
	G_SetOrigin( body, ent->r.currentOrigin );
	body->s.pos.trType = TR_GRAVITY;
	body->s.pos.trTime = level.time;
// Make sure to copy dead player's velocity to our body so it goes flying
	VectorCopy( ent->client->ps.velocity, body->s.pos.trDelta );

	body->s.event = 0;

	switch ( body->s.legsAnim & ~ANIM_TOGGLEBIT ) {
	case BOTH_DEATH1:
	case BOTH_DEAD1:
		body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD1;
		break;
	case BOTH_DEATH2:
	case BOTH_DEAD2:
		body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD2;
		break;
	case BOTH_DEATH3:
	case BOTH_DEAD3:
	default:
		body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD3;
		break;
	}

	body->r.svFlags = ent->r.svFlags;
// I am making the body have the default player's bounding box
// This isn't right though
// Could put body outside map so I need to fix this for later version
	VectorSet ( body->r.mins, -15, -15, -24 );
	VectorSet ( body->r.maxs, 15, 15, 32 );
	VectorCopy( ent->r.absmin, body->r.absmin );
	VectorCopy( ent->r.absmax, body->r.absmax );

	body->clipmask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP;
// Make the body something solid
	body->r.contents = CONTENTS_BODY;
	body->r.ownerNum = ent->s.number;

	body->nextthink = level.time + FRAMETIME;
	body->think = BodyThink;

	body->die = Body_die;

	body->takedamage = qtrue;

	trap_LinkEntity( body );

	body->noise_index = G_SoundIndex( "sound/player/tankjr/jump1.wav" );
// I use splashDamage to remember what team this body is on
	body->splashDamage = level.clients[ ent->s.clientNum ].sess.sessionTeam;
// This will be used for when you look at the body and see a player's name
	body->s.otherEntityNum = ent->s.number;

	ent->health = GIB_HEALTH;
// Don't let bullets and rockets hit us
	ent->r.contents = 0;
// So they don't move after free to move around (push from explosion they died from)
	VectorClear( ent->client->ps.velocity );
}


When I set the player's health to GIB_HEALTH meant that in bg_misc.c BG_PlayerStateToEntityState the player goes invisible so as they noclip around you can't see them.

	if ( ps->pm_type == PM_INTERMISSION || ps->pm_type == PM_SPECTATOR ) {
		s->eType = ET_INVISIBLE;
	} else if ( ps->stats[STAT_HEALTH] <= GIB_HEALTH ) {
		s->eType = ET_INVISIBLE;
	} else {
		s->eType = ET_PLAYER;
	}

Vondi's Note: I quakestyle'd this tutorial and i had to denote the "new code" and the "old code" BUT i've never even looked at the q3a code so... erm.. its probably amazingly innacurate, i apolagise. Email me any corrections. Oh and i don't understand this code either so it could make a cluster chicken launcher for all i know.

-- Credits:
   Tutorial by Doolittle
   Return to QS Tutorials

-- Important:
   If you do use something from QuakeStyle in your mod, please give us credit.
   Our code is copyrighted, but we give permission to everyone to use it in any way they see fit, as long as we are recognized.