Lightning Discharge

Hello people, this is actually SumFuka taking you through this tutorial but all the code is The SARACEN’s. There’s some real interesting stuff here… we’re essentially adding a new ‚event‘ to the game. This requires modifications to both the game dll/qvm (so that the effect gets triggered) and the cgame dll/qvm (so that the client can see the effect). This is gonna be fun, „unless you is a complete monga“.

Updated for the 1.27g source code. The function CG_SmokePuff() now takes an extra argument for the fade in time for the graphic.

1. Define Lightning Discharge ‚Event‘ and ‚MOD‘

Firstly, go into the game project and pull up bg_public.h. At line 345 we’re going to add another ‚event type‘, just below the other weapon events (rail trails, shotty sprays, bullet marks etc).

If the C syntax is baffling you, we are defining unique constants for the entity_event_t datatype. Have a browse through some of the other event types… not all of them are ‚visible things‘ (e.g. EV_NOAMMO) but they all do have something in common – when these events happen the clients need to be ‚told‘ about it.

	EV_MISSILE_HIT,
	EV_MISSILE_MISS,
	EV_RAILTRAIL,
	EV_SHOTGUN,
	EV_BULLET,				// otherEntity is the shooter
	EV_LIGHTNING_DISCHARGE,		// The SARACEN's Lightning Discharge

	EV_PAIN,
	EV_DEATH1,
	EV_DEATH2,
	EV_DEATH3,
	EV_OBITUARY,

Similarly, we’re going to add another meansOfDeath_t. The meansOfDeath or MOD is used when someone dies to pick the appropriate death message (remember ClientObituary from quake and quake2 ? Kinda similar…).

// means of death
typedef enum {
	MOD_UNKNOWN,
	MOD_SHOTGUN,
	MOD_GAUNTLET,
	MOD_MACHINEGUN,
	MOD_GRENADE,
	MOD_GRENADE_SPLASH,
	MOD_ROCKET,
	MOD_ROCKET_SPLASH,
	MOD_PLASMA,
	MOD_PLASMA_SPLASH,
	MOD_RAILGUN,
	MOD_LIGHTNING,
	MOD_LIGHTNING_DISCHARGE,	// The SARACEN's Lightning Discharge
	MOD_BFG,
	MOD_BFG_SPLASH,
	MOD_WATER,
	MOD_SLIME,
	MOD_LAVA,
	MOD_CRUSH,
	MOD_TELEFRAG,
	MOD_FALLING,
	MOD_SUICIDE,
	MOD_TARGET_LASER,
	MOD_TRIGGER_HURT,
	MOD_GRAPPLE
} meansOfDeath_t;

Now open up combat.c and at line 159 insert a string for our new MOD as below. (Why do we need this as well as the modification above ? Simply, the EV_MOD’s are constant values and since they aren’t strings they can’t be used in game messages. We define some useful error strings here for that purpose).

	"MOD_RAILGUN",
	"MOD_LIGHTNING",
	"MOD_LIGHTNING_DISCHARGE",		// The SARACEN's Lightning Discharge
	"MOD_BFG",
	"MOD_BFG_SPLASH",

2. Implement a Water Radius Damage Function

Still in game and g_combat.c, go to line 733 and add the following new function directly after G_RadiusDamage :

/*
============
G_WaterRadiusDamage for The SARACEN's Lightning Discharge
============
*/
qboolean G_WaterRadiusDamage (vec3_t origin, gentity_t *attacker, float damage, float radius,
					 gentity_t *ignore, int mod)
{
	float		points, dist;
	gentity_t	*ent;
	int		entityList[MAX_GENTITIES];
	int		numListedEntities;
	vec3_t		mins, maxs;
	vec3_t		v;
	vec3_t		dir;
	int		i, e;
	qboolean	hitClient = qfalse;

	if (!(trap_PointContents (origin, -1) & MASK_WATER)) return qfalse;
		// if we're not underwater, forget it!

	if (radius < 1) radius = 1;

	for (i = 0 ; i < 3 ; i++)
	{
		mins[i] = origin[i] - radius;
		maxs[i] = origin[i] + radius;
	}

	numListedEntities = trap_EntitiesInBox (mins, maxs, entityList, MAX_GENTITIES);

	for (e = 0 ; e < numListedEntities ; e++)
	{
		ent = &g_entities[entityList[e]];

		if (ent == ignore)			continue;
		if (!ent->takedamage)		continue;

		// find the distance from the edge of the bounding box
		for (i = 0 ; i < 3 ; i++)
		{
			     if (origin[i] < ent->r.absmin[i]) v[i] = ent->r.absmin[i] - origin[i];
			else if (origin[i] > ent->r.absmax[i]) v[i] = origin[i] - ent->r.absmax[i];
			else v[i] = 0;
		}

		dist = VectorLength(v);
		if (dist >= radius)			continue;

		points = damage * (1.0 - dist / radius);

		if (CanDamage (ent, origin) && ent->waterlevel) 	// must be in the water, somehow!
		{
			if (LogAccuracyHit (ent, attacker)) hitClient = qtrue;
			VectorSubtract (ent->r.currentOrigin, origin, dir);
			// push the center of mass higher than the origin so players
			// get knocked into the air more
			dir[2] += 24;
			G_Damage (ent, NULL, attacker, dir, origin, (int)points, DAMAGE_RADIUS, mod);
		}
	}

	return hitClient;
}

3. Modify the Lightning Gun Fire Function

Open up g_weapon.c and find Weapon_LightningFire at line 475. We need to modify the weapon so that it does a G_WaterRadiusDamage if fired underwater.

/*
======================================================================

LIGHTNING GUN

======================================================================
*/

void Weapon_LightningFire( gentity_t *ent ) {
	trace_t		tr;
	vec3_t		end;
	gentity_t	*traceEnt, *tent;
	int			damage;

	damage = 8 * s_quadFactor;

	VectorMA( muzzle, LIGHTNING_RANGE, forward, end );

// The SARACEN's Lightning Discharge - START
	if (trap_PointContents (muzzle, -1) & MASK_WATER)
	{
		int zaps;
		gentity_t *tent;

		zaps = ent->client->ps.ammo[WP_LIGHTNING];	// determines size/power of discharge
		if (!zaps) return;	// prevents any subsequent frames causing second discharge + error
		zaps++;		// pmove does an ammo[gun]--, so we must compensate
		SnapVectorTowards (muzzle, ent->s.origin);	// save bandwidth

		tent = G_TempEntity (muzzle, EV_LIGHTNING_DISCHARGE);
		tent->s.eventParm = zaps;				// duration / size of explosion graphic

		ent->client->ps.ammo[WP_LIGHTNING] = 0;		// drain ent's lightning count
		if (G_WaterRadiusDamage (muzzle, ent, damage * zaps,
					(damage * zaps) + 16, NULL, MOD_LIGHTNING_DISCHARGE))
			g_entities[ent->r.ownerNum].client->ps.persistant[PERS_ACCURACY_HITS]++;
		
		return;
	}
// The SARACEN's Lightning Discharge - END

	trap_Trace( &tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT );

	if ( tr.entityNum == ENTITYNUM_NONE ) {
		return;
	}

	traceEnt = &g_entities[ tr.entityNum ];

	if ( traceEnt->takedamage && traceEnt->client ) {
		tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT );
		tent->s.otherEntityNum = traceEnt->s.number;
		tent->s.eventParm = DirToByte( tr.plane.normal );
		tent->s.weapon = ent->s.weapon;
		if( LogAccuracyHit( traceEnt, ent ) ) {
			ent->client->ps.persistant[PERS_ACCURACY_HITS]++;
		}
	} else if ( !( tr.surfaceFlags & SURF_NOIMPACT ) ) {
		tent = G_TempEntity( tr.endpos, EV_MISSILE_MISS );
		tent->s.eventParm = DirToByte( tr.plane.normal );
	}

	if ( traceEnt->takedamage) {
		G_Damage( traceEnt, ent, ent, forward, tr.endpos,
			damage, 0, MOD_LIGHTNING);
	}
}

The bit The SARACEN added is quite straightforward – first use the trap_PointContents function to test if the weapon is being fired in the water. A temp entity EV_LIGHTNING_DISCHARGE is then created and automatically broadcast to the clients (so that they see it). Then, we do a G_WaterRadiusDamage proportional to the amount of ammo the player has remaining (zaps). Anyone within range should be fried.

3. Implement the Death Messages

Ok, go into the cgame project and open up cg_local.h. First, add the following function prototype at line 1023 :

localEntity_t *CG_MakeExplosion( vec3_t origin, vec3_t dir,
								qhandle_t hModel, qhandle_t shader, int msec,
								qboolean isSprite );

void CG_Lightning_Discharge (vec3_t origin, int msec); 	// The SARACEN's Lightning Discharge

Now open cg_event.c and go to line 155. We need to add some death messages for our new meansOfDeath :

		case MOD_PLASMA_SPLASH:
			if ( gender == GENDER_FEMALE )
				message = "melted herself";
			else if ( gender == GENDER_NEUTER )
				message = "melted itself";
			else
				message = "melted himself";
			break;

// The SARACEN's Lightning Discharge - START
		case MOD_LIGHTNING_DISCHARGE:
			if (gender == GENDER_FEMALE)
				message = "discharged herself";
			else if (gender == GENDER_NEUTER)
				message = "discharged itself";
			else
				message = "discharged himself";
			break;
// The SARACEN's Lightning Discharge - END

		case MOD_BFG_SPLASH:
			message = "should have used a smaller gun";
			break;
		default:
			if ( gender == GENDER_FEMALE )
				message = "killed herself";
			else if ( gender == GENDER_NEUTER )
				message = "killed itself";
			else
				message = "killed himself";
			break;

Now at line 255 The SARACEN has had some more fun with the death messages, nice one :

		case MOD_RAILGUN:
			message = "was railed by";
			break;
// The SARACEN's Lightning Discharge - START
/*	// original obituary
		case MOD_LIGHTNING:
			message = "was electrocuted by";
			break;
*/	// Classic Quake style obituary - the original and the best!!!
		case MOD_LIGHTNING:
			message = "was shafted by";
			break;
		case MOD_LIGHTNING_DISCHARGE:
			message = "was discharged by";
			break;
// The SARACEN's Lightning Discharge - END
		case MOD_BFG:
		case MOD_BFG_SPLASH:
			message = "was blasted by";
			message2 = "'s BFG";
			break;

4. Add an Event Hook

Now at line 780 we need to define which function is called when a certain event is triggered (our lightning discharge!).

	case EV_SHOTGUN:
		DEBUGNAME("EV_SHOTGUN");
		CG_ShotgunFire( es );
		break;

// The SARACEN's Lightning Discharge - START
	case EV_LIGHTNING_DISCHARGE:
		DEBUGNAME("EV_LIGHTNING_DISCHARGE");
		CG_Lightning_Discharge (position, es->eventParm);	// eventParm is duration/size
		break;
// The SARACEN's Lightning Discharge - END

	case EV_GENERAL_SOUND:
		DEBUGNAME("EV_GENERAL_SOUND");
		if ( cgs.gameSounds[ es->eventParm ] ) {
			trap_S_StartSound (NULL, es->number, CHAN_VOICE, cgs.gameSounds[ es->eventParm ] );
		} else {
			s = CG_ConfigString( CS_SOUNDS + es->eventParm );
			trap_S_StartSound (NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, s ) );
		}
		break;

5. Prevent Client from Drawing a Shaft

Ok now we need to stop the client from drawing a shaft on the screen if the gun is fired underwater (remember we previously modified the firing behaviour in the game project, and all the visuals are done in the cgame project which we are now working with).

/*
===============
CG_LightningBolt

Origin will be the exact tag point, which is slightly
different than the muzzle point used for determining hits.
The cent should be the non-predicted cent if it is from the player,
so the endpoint will reflect the simulated strike (lagging the predicted
angle)
===============
*/
static void CG_LightningBolt( centity_t *cent, vec3_t origin ) {
	trace_t		trace;
	refEntity_t		beam;
	vec3_t			forward;
	vec3_t			muzzlePoint, endPoint;

	if ( cent->currentState.weapon != WP_LIGHTNING ) {
		return;
	}

	memset( &beam, 0, sizeof( beam ) );

	// find muzzle point for this frame
	VectorCopy( cent->lerpOrigin, muzzlePoint );
	AngleVectors( cent->lerpAngles, forward, NULL, NULL );

	// FIXME: crouch
	muzzlePoint[2] += DEFAULT_VIEWHEIGHT;

	VectorMA( muzzlePoint, 14, forward, muzzlePoint );

// The SARACEN's Lightning Discharge
	if (trap_CM_PointContents (muzzlePoint, 0) & MASK_WATER) return;

	// project forward by the lightning range
	VectorMA( muzzlePoint, LIGHTNING_RANGE, forward, endPoint );

	// see if it hit a wall
	CG_Trace( &trace, muzzlePoint, vec3_origin, vec3_origin, endPoint, 
		cent->currentState.number, MASK_SHOT );

	// this is the endpoint
	VectorCopy( trace.endpos, beam.oldorigin );

6. Draw the Effect

Open up cg_effect.c and go to line 166 and let’s add some code directly after the CG_SpawnEffect function.

/*
====================
CG_Lightning_Discharge by The SARACEN
====================
*/
void CG_Lightning_Discharge (vec3_t origin, int msec)
{
	localEntity_t		*le;

	if (msec <= 0) CG_Error ("CG_Lightning_Discharge: msec = %i", msec);

	le = CG_SmokePuff (	origin,			// where
				vec3_origin,			// where to
				((48 + (msec * 10)) / 16),	// radius
				1, 1, 1, 1,			// RGBA color shift
				300 + msec,			// duration
				cg.time,			// start when?
				0,					// fade in time
				0,				// flags (?)
				trap_R_RegisterShader ("models/weaphits/electric.tga"));

	le->leType = LE_SCALE_FADE;
}

What does this do ? The SARACEN has created a big smoke puff that lasts a defined number of milliseconds (proportional the the strength of the discharge – in other words, the amount of ammo that was discharged).

That’s it ! Once again, thanks to The SARACEN for this great code and I hope that my (me==SumFuka) explanations did it proper justice. Now go play who’s-gonna-get-the-red-armor on the level with the red armor in the bottom of the water pool.