TUTORIAL 13 - Lightning Discharge
by The SARACEN
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.
|