jkxr/Projects/Android/jni/OpenJK/codeJK2/game/g_fx.cpp

806 lines
22 KiB
C++
Raw Normal View History

/*
===========================================================================
Copyright (C) 2000 - 2013, Raven Software, Inc.
Copyright (C) 2001 - 2013, Activision, Inc.
Copyright (C) 2013 - 2015, OpenJK contributors
This file is part of the OpenJK source code.
OpenJK is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, see <http://www.gnu.org/licenses/>.
===========================================================================
*/
#include "g_headers.h"
#include "g_local.h"
#include "g_functions.h"
extern int G_FindConfigstringIndex( const char *name, int start, int max, qboolean create );
#define FX_ENT_RADIUS 32
extern int BMS_START;
extern int BMS_MID;
extern int BMS_END;
//----------------------------------------------------------
/*QUAKED fx_runner (0 0 1) (-8 -8 -8) (8 8 8) STARTOFF ONESHOT DAMAGE
Runs the specified effect, can also be targeted at an info_notnull to orient the effect
STARTOFF - effect starts off, toggles on/off when used
ONESHOT - effect fires only when used
DAMAGE - does radius damage around effect every "delay" milliseonds
"fxFile" - name of the effect file to play
"target" - direction to aim the effect in, otherwise defaults to up
"target2" - uses its target2 when the fx gets triggered
"delay" - how often to call the effect, don't over-do this ( default 200 )
"random" - random amount of time to add to delay, ( default 0, 200 = 0ms to 200ms )
"splashRadius" - only works when damage is checked ( default 16 )
"splashDamage" - only works when damage is checked ( default 5 )
"soundset" - bmodel set to use, plays start sound when toggled on, loop sound while on ( doesn't play on a oneshot), and a stop sound when turned off
*/
#define FX_RUNNER_RESERVED 0x800000
//----------------------------------------------------------
void fx_runner_think( gentity_t *ent )
{
vec3_t temp;
EvaluateTrajectory( &ent->s.pos, level.time, ent->currentOrigin );
EvaluateTrajectory( &ent->s.apos, level.time, ent->currentAngles );
// call the effect with the desired position and orientation
G_AddEvent( ent, EV_PLAY_EFFECT, ent->fxID );
// Assume angles, we'll do a cross product on the other end to finish up
AngleVectors( ent->currentAngles, ent->pos3, NULL, NULL );
MakeNormalVectors( ent->pos3, ent->pos4, temp ); // there IS a reason this is done...it's so that it doesn't break every effect in the game...
ent->nextthink = level.time + ent->delay + Q_flrand(0.0f, 1.0f) * ent->random;
if ( ent->spawnflags & 4 ) // damage
{
G_RadiusDamage( ent->currentOrigin, ent, ent->splashDamage, ent->splashRadius, ent, MOD_UNKNOWN );
}
if ( ent->target2 )
{
// let our target know that we have spawned an effect
G_UseTargets2( ent, ent, ent->target2 );
}
if ( !(ent->spawnflags & 2 ) && !ent->s.loopSound ) // NOT ONESHOT...this is an assy thing to do
{
if ( VALIDSTRING( ent->soundSet ) == true )
{
ent->s.loopSound = CAS_GetBModelSound( ent->soundSet, BMS_MID );
if ( ent->s.loopSound < 0 )
{
ent->s.loopSound = 0;
}
}
}
}
//----------------------------------------------------------
void fx_runner_use( gentity_t *self, gentity_t *other, gentity_t *activator )
{
if ( self->spawnflags & 2 ) // ONESHOT
{
// call the effect with the desired position and orientation, as a safety thing,
// make sure we aren't thinking at all.
fx_runner_think( self );
self->nextthink = -1;
if ( self->target2 )
{
// let our target know that we have spawned an effect
G_UseTargets2( self, self, self->target2 );
}
if ( VALIDSTRING( self->soundSet ) == true )
{
G_AddEvent( self, EV_BMODEL_SOUND, CAS_GetBModelSound( self->soundSet, BMS_START ));
}
}
else
{
// ensure we are working with the right think function
self->e_ThinkFunc = thinkF_fx_runner_think;
// toggle our state
if ( self->nextthink == -1 )
{
// NOTE: we fire the effect immediately on use, the fx_runner_think func will set
// up the nextthink time.
fx_runner_think( self );
if ( VALIDSTRING( self->soundSet ) == true )
{
G_AddEvent( self, EV_BMODEL_SOUND, CAS_GetBModelSound( self->soundSet, BMS_START ));
self->s.loopSound = CAS_GetBModelSound( self->soundSet, BMS_MID );
if ( self->s.loopSound < 0 )
{
self->s.loopSound = 0;
}
}
}
else
{
// turn off for now
self->nextthink = -1;
if ( VALIDSTRING( self->soundSet ) == true )
{
G_AddEvent( self, EV_BMODEL_SOUND, CAS_GetBModelSound( self->soundSet, BMS_END ));
self->s.loopSound = 0;
}
}
}
}
//----------------------------------------------------------
void fx_runner_link( gentity_t *ent )
{
vec3_t dir;
if ( ent->target )
{
// try to use the target to override the orientation
gentity_t *target = NULL;
target = G_Find( target, FOFS(targetname), ent->target );
if ( !target )
{
// Bah, no good, dump a warning, but continue on and use the UP vector
Com_Printf( "fx_runner_link: target specified but not found: %s\n", ent->target );
Com_Printf( " -assuming UP orientation.\n" );
}
else
{
// Our target is valid so let's override the default UP vector
VectorSubtract( target->s.origin, ent->s.origin, dir );
VectorNormalize( dir );
vectoangles( dir, ent->s.angles );
}
}
// don't really do anything with this right now other than do a check to warn the designers if the target2 is bogus
if ( ent->target2 )
{
gentity_t *target = NULL;
target = G_Find( target, FOFS(targetname), ent->target2 );
if ( !target )
{
// Target2 is bogus, but we can still continue
Com_Printf( "fx_runner_link: target2 was specified but is not valid: %s\n", ent->target2 );
}
}
G_SetAngles( ent, ent->s.angles );
if ( ent->spawnflags & 1 || ent->spawnflags & 2 ) // STARTOFF || ONESHOT
{
// We won't even consider thinking until we are used
ent->nextthink = -1;
}
else
{
if ( VALIDSTRING( ent->soundSet ) == true )
{
ent->s.loopSound = CAS_GetBModelSound( ent->soundSet, BMS_MID );
if ( ent->s.loopSound < 0 )
{
ent->s.loopSound = 0;
}
}
// Let's get to work right now!
ent->e_ThinkFunc = thinkF_fx_runner_think;
ent->nextthink = level.time + 200; // wait a small bit, then start working
}
// make us useable if we can be targeted
if ( ent->targetname )
{
ent->e_UseFunc = useF_fx_runner_use;
}
}
//----------------------------------------------------------
void SP_fx_runner( gentity_t *ent )
{
// Get our defaults
G_SpawnInt( "delay", "200", &ent->delay );
G_SpawnFloat( "random", "0", &ent->random );
G_SpawnInt( "splashRadius", "16", &ent->splashRadius );
G_SpawnInt( "splashDamage", "5", &ent->splashDamage );
if ( !G_SpawnAngleHack( "angle", "0", ent->s.angles ))
{
// didn't have angles, so give us the default of up
VectorSet( ent->s.angles, -90, 0, 0 );
}
if ( !ent->fxFile )
{
gi.Printf( S_COLOR_RED"ERROR: fx_runner %s at %s has no fxFile specified\n", ent->targetname, vtos(ent->s.origin) );
G_FreeEntity( ent );
return;
}
// Try and associate an effect file, unfortunately we won't know if this worked or not
// until the CGAME trys to register it...
ent->fxID = G_EffectIndex( ent->fxFile );
ent->s.eType = ET_MOVER;
// Give us a bit of time to spawn in the other entities, since we may have to target one of 'em
ent->e_ThinkFunc = thinkF_fx_runner_link;
ent->nextthink = level.time + 400;
// Save our position and link us up!
G_SetOrigin( ent, ent->s.origin );
VectorSet( ent->maxs, FX_ENT_RADIUS, FX_ENT_RADIUS, FX_ENT_RADIUS );
VectorScale( ent->maxs, -1, ent->mins );
gi.linkentity( ent );
}
/*QUAKED fx_snow (1 0 0) (-16 -16 -16) (16 16 16)
This world effect will spawn snow globally into the level.
"count" the number of snow particles (default of 1000)
*/
//----------------------------------------------------------
void SP_CreateSnow( gentity_t *ent )
{
char temp[256];
G_SpawnInt( "count", "1000", &ent->count );
cvar_t *r_weatherScale = gi.cvar( "r_weatherScale", "1", CVAR_ARCHIVE );
if ( r_weatherScale->value > 0.0f )
{
sprintf( temp, "snow init %i", (int)( ent->count * r_weatherScale->value ));
G_FindConfigstringIndex( temp, CS_WORLD_FX, MAX_WORLD_FX, qtrue );
level.worldFlags |= WF_SNOWING;
}
}
/*QUAKED fx_rain (1 0 0) (-16 -16 -16) (16 16 16)
This world effect will spawn rain globally into the level.
"count" the number of rain particles (default of 500)
*/
//----------------------------------------------------------
void SP_CreateRain( gentity_t *ent )
{
char temp[256];
G_SpawnInt( "count", "500", &ent->count );
cvar_t *r_weatherScale = gi.cvar( "r_weatherScale", "1", CVAR_ARCHIVE );
if ( r_weatherScale->value > 0.0f )
{
sprintf( temp, "rain init %i", (int)( ent->count * r_weatherScale->value ));
G_FindConfigstringIndex( temp, CS_WORLD_FX, MAX_WORLD_FX, qtrue );
level.worldFlags |= WF_RAINING;
}
}
//-----------------
// Explosion Trail
//-----------------
//----------------------------------------------------------
void fx_explosion_trail_think( gentity_t *ent )
{
vec3_t origin;
trace_t tr;
if ( ent->spawnflags & 1 ) // gravity
{
ent->s.pos.trType = TR_GRAVITY;
}
else
{
ent->s.pos.trType = TR_LINEAR;
}
EvaluateTrajectory( &ent->s.pos, level.time, origin );
gi.trace( &tr, ent->currentOrigin, vec3_origin, vec3_origin, origin,
ent->owner ? ent->owner->s.number : ENTITYNUM_NONE, ent->clipmask, G2_RETURNONHIT, 10 );
if ( tr.fraction < 1.0f )
{
// never explode or bounce on sky
if ( !( tr.surfaceFlags & SURF_NOIMPACT ))
{
if ( ent->splashDamage && ent->splashRadius )
{
G_RadiusDamage( tr.endpos, ent, ent->splashDamage, ent->splashRadius, ent, MOD_EXPLOSIVE_SPLASH );
}
}
if ( ent->fullName )
{
// fxFile2....in other words, impact fx
G_PlayEffect( ent->fullName, tr.endpos, tr.plane.normal );
}
if ( VALIDSTRING( ent->soundSet ) == true )
{
G_AddEvent( ent, EV_BMODEL_SOUND, CAS_GetBModelSound( ent->soundSet, BMS_END ));
}
G_FreeEntity( ent );
return;
}
G_RadiusDamage( origin, ent, ent->damage, ent->radius, ent, MOD_EXPLOSIVE_SPLASH );
// call the effect with the desired position and orientation
G_PlayEffect( ent->fxID, origin, ent->currentAngles );
ent->nextthink = level.time + 50;
gi.linkentity( ent );
}
//----------------------------------------------------------
void fx_explosion_trail_use( gentity_t *self, gentity_t *other, gentity_t *activator )
{
gentity_t *missile = G_Spawn();
// We aren't a missile in the truest sense, rather we just move through the world and spawn effects
if ( missile )
{
missile->classname = "fx_exp_trail";
missile->nextthink = level.time + 50;
missile->e_ThinkFunc = thinkF_fx_explosion_trail_think;
missile->s.eType = ET_MOVER;
missile->owner = self;
missile->s.modelindex = self->s.modelindex2;
missile->s.pos.trTime = level.time;
G_SetOrigin( missile, self->currentOrigin );
if ( self->spawnflags & 1 ) // gravity
{
missile->s.pos.trType = TR_GRAVITY;
}
else
{
missile->s.pos.trType = TR_LINEAR;
}
missile->spawnflags = self->spawnflags;
G_SetAngles( missile, self->currentAngles );
VectorScale( self->currentAngles, self->speed, missile->s.pos.trDelta );
missile->s.pos.trTime = level.time;
missile->radius = self->radius;
missile->damage = self->damage;
missile->splashDamage = self->splashDamage;
missile->splashRadius = self->splashRadius;
missile->fxID = self->fxID;
missile->fullName = self->fullName;
missile->clipmask = MASK_SHOT;
gi.linkentity( missile );
if ( VALIDSTRING( self->soundSet ) == true )
{
G_AddEvent( self, EV_BMODEL_SOUND, CAS_GetBModelSound( self->soundSet, BMS_START ));
missile->s.loopSound = CAS_GetBModelSound( self->soundSet, BMS_MID );
missile->soundSet = self->soundSet;
if ( missile->s.loopSound < 0 )
{
missile->s.loopSound = 0;
}
}
}
}
//----------------------------------------------------------
void fx_explosion_trail_link( gentity_t *ent )
{
vec3_t dir;
gentity_t *target = NULL;
// we ony activate when used
ent->e_UseFunc = useF_fx_explosion_trail_use;
if ( ent->target )
{
// try to use the target to override the orientation
target = G_Find( target, FOFS(targetname), ent->target );
if ( !target )
{
gi.Printf( S_COLOR_RED"ERROR: fx_explosion_trail %s could not find target %s\n", ent->targetname, ent->target );
G_FreeEntity( ent );
return;
}
// Our target is valid so lets use that
VectorSubtract( target->s.origin, ent->s.origin, dir );
VectorNormalize( dir );
}
else
{
// we are assuming that we have angles, but there are no checks to verify this
AngleVectors( ent->s.angles, dir, NULL, NULL );
}
// NOTE: this really isn't an angle, but rather an orientation vector
G_SetAngles( ent, dir );
}
/*QUAKED fx_explosion_trail (0 0 1) (-8 -8 -8) (8 8 8) GRAVITY
Creates an explosion type trail using the specified effect file, damaging things as it moves through the environment
Can also be used for something like a meteor, just add an impact effect ( fxFile2 ) and a splashDamage and splashRadius
GRAVITY - object uses gravity instead of linear motion
"fxFile" - name of the effect to play for the trail ( default "env/exp_trail_comp" )
"fxFile2" - effect file to play on impact
"model" - model to attach to the trail
"target" - direction to aim the trail in, required unless you specify angles
"targetname" - (required) trail effect spawns only when used.
"speed" - velocity through the world, ( default 350 )
"radius" - damage radius around trail as it travels through the world ( default 128 )
"damage" - radius damage ( default 128 )
"splashDamage" - damage when thing impacts ( default 0 )
"splashRadius" - damage radius on impact ( default 0 )
"soundset" - soundset to use, start sound plays when explosion trail starts, loop sound plays on explosion trail, end sound plays when it impacts
*/
//----------------------------------------------------------
void SP_fx_explosion_trail( gentity_t *ent )
{
// We have to be useable, otherwise we won't spawn in
if ( !ent->targetname )
{
gi.Printf( S_COLOR_RED"ERROR: fx_explosion_trail at %s has no targetname specified\n", vtos( ent->s.origin ));
G_FreeEntity( ent );
return;
}
// Get our defaults
G_SpawnString( "fxFile", "env/exp_trail_comp", &ent->fxFile );
G_SpawnInt( "damage", "128", &ent->damage );
G_SpawnFloat( "radius", "128", &ent->radius );
G_SpawnFloat( "speed", "350", &ent->speed );
// Try and associate an effect file, unfortunately we won't know if this worked or not until the CGAME trys to register it...
ent->fxID = G_EffectIndex( ent->fxFile );
if ( ent->fullName )
{
G_EffectIndex( ent->fullName );
}
if ( ent->model )
{
ent->s.modelindex2 = G_ModelIndex( ent->model );
}
// Give us a bit of time to spawn in the other entities, since we may have to target one of 'em
ent->e_ThinkFunc = thinkF_fx_explosion_trail_link;
ent->nextthink = level.time + 500;
// Save our position and link us up!
G_SetOrigin( ent, ent->s.origin );
VectorSet( ent->maxs, FX_ENT_RADIUS, FX_ENT_RADIUS, FX_ENT_RADIUS );
VectorScale( ent->maxs, -1, ent->mins );
gi.linkentity( ent );
}
//
//
//------------------------------------------
void fx_target_beam_set_debounce( gentity_t *self )
{
if ( self->wait >= FRAMETIME )
{
self->attackDebounceTime = level.time + self->wait + Q_irand( -self->random, self->random );
}
else if ( self->wait < 0 )
{
self->e_UseFunc = useF_NULL;
}
else
{
self->attackDebounceTime = level.time + FRAMETIME + Q_irand( -self->random, self->random );
}
}
//------------------------------------------
void fx_target_beam_fire( gentity_t *ent )
{
trace_t trace;
vec3_t dir, org, end;
qboolean open;
if ( !ent->enemy || !ent->enemy->inuse )
{//info_null most likely
ent->enemy = NULL;
VectorCopy( ent->s.origin2, org );
}
else
{
VectorCopy( ent->enemy->currentOrigin, org );
}
VectorCopy( org, ent->s.origin2 );
VectorSubtract( org, ent->s.origin, dir );
VectorNormalize( dir );
gi.trace( &trace, ent->s.origin, NULL, NULL, org, ENTITYNUM_NONE, MASK_SHOT, G2_NOCOLLIDE, 0 );//ignore
if ( ent->spawnflags & 2 )
{
open = qtrue;
VectorCopy( org, end );
}
else
{
open = qfalse;
VectorCopy( trace.endpos, end );
}
if ( trace.fraction < 1.0 )
{
if ( trace.entityNum < ENTITYNUM_WORLD )
{
gentity_t *victim = &g_entities[trace.entityNum];
if ( victim && victim->takedamage )
{
if ( ent->spawnflags & 4 ) // NO_KNOCKBACK
{
G_Damage( victim, ent, ent->activator, dir, trace.endpos, ent->damage, DAMAGE_NO_KNOCKBACK, MOD_UNKNOWN );
}
else
{
G_Damage( victim, ent, ent->activator, dir, trace.endpos, ent->damage, 0, MOD_UNKNOWN );
}
}
}
}
G_AddEvent( ent, EV_TARGET_BEAM_DRAW, ent->fxID );
VectorCopy( end, ent->s.origin2 );
if ( open )
{
VectorScale( dir, -1, ent->pos1 );
}
else
{
VectorCopy( trace.plane.normal, ent->pos1 );
}
ent->e_ThinkFunc = thinkF_fx_target_beam_think;
ent->nextthink = level.time + FRAMETIME;
}
//------------------------------------------
void fx_target_beam_fire_start( gentity_t *self )
{
fx_target_beam_set_debounce( self );
self->e_ThinkFunc = thinkF_fx_target_beam_think;
self->nextthink = level.time + FRAMETIME;
self->painDebounceTime = level.time + self->speed + Q_irand( -500, 500 );
fx_target_beam_fire( self );
}
//------------------------------------------
void fx_target_beam_use( gentity_t *self, gentity_t *other, gentity_t *activator )
{
if ( self->spawnflags & 8 ) // one shot
{
fx_target_beam_fire( self );
self->e_ThinkFunc = thinkF_NULL;
}
else if ( self->e_ThinkFunc == thinkF_NULL )
{
self->e_ThinkFunc = thinkF_fx_target_beam_think;
self->nextthink = level.time + 50;
}
else
{
self->e_ThinkFunc = thinkF_NULL;
}
self->activator = activator;
}
//------------------------------------------
void fx_target_beam_think( gentity_t *ent )
{
if ( ent->attackDebounceTime > level.time )
{
ent->nextthink = level.time + FRAMETIME;
return;
}
fx_target_beam_fire_start( ent );
}
//------------------------------------------
void fx_target_beam_link( gentity_t *ent )
{
gentity_t *target = NULL;
vec3_t dir;
target = G_Find( target, FOFS(targetname), ent->target );
if ( !target )
{
Com_Printf( "bolt_link: unable to find target %s\n", ent->target );
G_FreeEntity( ent );
return;
}
ent->attackDebounceTime = level.time;
if ( !target->classname || Q_stricmp( "info_null", target->classname ) )
{//don't want to set enemy to something that's going to free itself... actually, this could be bad in other ways, too... ent pointer could be freed up and re-used by the time we check it next
G_SetEnemy( ent, target );
}
VectorSubtract( target->s.origin, ent->s.origin, dir );//er, does it ever use dir?
VectorNormalize( dir );//er, does it use len or dir?
vectoangles( dir, ent->s.angles );//er, does it use s.angles?
VectorCopy( target->s.origin, ent->s.origin2 );
if ( ent->spawnflags & 1 )
{
// Do nothing
ent->e_ThinkFunc = thinkF_NULL;
}
else
{
if ( !(ent->spawnflags & 8 )) // one_shot, only calls when used
{
// switch think functions to avoid doing the bolt_link every time
ent->e_ThinkFunc = thinkF_fx_target_beam_think;
ent->nextthink = level.time + FRAMETIME;
}
}
ent->e_UseFunc = useF_fx_target_beam_use;
gi.linkentity( ent );
}
/*QUAKED fx_target_beam (1 0.5 0.5) (-8 -8 -8) (8 8 8) STARTOFF OPEN NO_KNOCKBACK ONE_SHOT NO_IMPACT
Emits specified effect file, doing damage if required
STARTOFF - must be used before it's on
OPEN - will draw all the way to the target, regardless of where the trace hits
NO_KNOCKBACK - beam damage does no knockback
"fxFile" - Effect used to draw the beam, ( default "env/targ_beam" )
"fxFile2" - Effect used for the beam impact effect, ( default "env/targ_beam_impact" )
"targetname" - Fires only when used
"duration" - How many seconds each burst lasts, -1 will make it stay on forever
"wait" - If always on, how long to wait between blasts, in MILLISECONDS - default/min is 100 (1 frame at 10 fps), -1 means it will never fire again
"random" - random amount of seconds added to/subtracted from "wait" each firing
"damage" - How much damage to inflict PER FRAME (so probably want it kind of low), default is none
"target" - ent to point at- you MUST have this. This can be anything you want, including a moving ent - for static beams, just use info_null
*/
//------------------------------------------
void SP_fx_target_beam( gentity_t *ent )
{
G_SetOrigin( ent, ent->s.origin );
ent->speed *= 1000;
ent->wait *= 1000;
ent->random *= 1000;
if ( ent->speed < FRAMETIME )
{
ent->speed = FRAMETIME;
}
G_SpawnInt( "damage", "0", &ent->damage );
G_SpawnString( "fxFile", "env/targ_beam", &ent->fxFile );
if ( ent->spawnflags & 16 ) // NO_IMPACT FX
{
ent->delay = 0;
}
else
{
G_SpawnString( "fxFile2", "env/targ_beam_impact", &ent->fullName );
ent->delay = G_EffectIndex( ent->fullName );
}
ent->fxID = G_EffectIndex( ent->fxFile );
ent->activator = ent;
ent->owner = NULL;
ent->e_ThinkFunc = thinkF_fx_target_beam_link;
ent->nextthink = level.time + START_TIME_LINK_ENTS;
VectorSet( ent->maxs, FX_ENT_RADIUS, FX_ENT_RADIUS, FX_ENT_RADIUS );
VectorScale( ent->maxs, -1, ent->mins );
gi.linkentity( ent );
}
/*QUAKED fx_cloudlayer (1 0.3 0.5) (-8 -8 -8) (8 8 8) TUBE ALT
Creates a scalable scrolling cloud layer, mostly for bespin undercity but could be used other places
TUBE - creates cloud layer with tube opening in the middle, must an INNER radius also
ALT - uses slightly different shader, good if using two layers sort of close together
"radius" - outer radius of cloud layer, (default 2048)
"random" - inner radius of cloud layer, (default 128) only works for TUBE type
"wait" - adds curvature as it moves out to the edge of the layer. ( default 0 ), 1 = small up, 3 = up more, -1 = small down, -3 = down more, etc.
*/
void SP_fx_cloudlayer( gentity_t *ent )
{
// HACK: this effect is never played, rather it just caches the shaders I need cgame side
G_EffectIndex( "world/haze_cache" );
G_SpawnFloat( "radius", "2048", &ent->radius );
G_SpawnFloat( "random", "128", &ent->random );
G_SpawnFloat( "wait", "0", &ent->wait );
ent->s.eType = ET_CLOUD; // dumb
G_SetOrigin( ent, ent->s.origin );
ent->contents = 0;
VectorSet( ent->maxs, 200, 200, 200 );
VectorScale( ent->maxs, -1, ent->mins );
gi.linkentity( ent );
}