701 lines
16 KiB
C++
701 lines
16 KiB
C++
#include "g_local.h"
|
|
#include "g_functions.h"
|
|
#include "g_infostrings.h"
|
|
#include "g_items.h"
|
|
|
|
extern void CrystalAmmoSettings(gentity_t *ent);
|
|
extern cvar_t *g_spskill;
|
|
|
|
/*
|
|
|
|
Items are any object that a player can touch to gain some effect.
|
|
|
|
Pickup will return the number of seconds until they should respawn.
|
|
|
|
all items should pop when dropped in lava or slime
|
|
|
|
Respawnable items don't actually go away when picked up, they are
|
|
just made invisible and untouchable. This allows them to ride
|
|
movers and respawn apropriately.
|
|
*/
|
|
|
|
// Item Spawn flags
|
|
#define ITMSF_SUSPEND 1
|
|
#define ITMSF_TEAM 2
|
|
#define ITMSF_MONSTER 4
|
|
#define ITMSF_NOTSOLID 8
|
|
#define ITMSF_VERTICAL 16
|
|
#define ITMSF_INVISIBLE 32
|
|
|
|
//======================================================================
|
|
|
|
int Pickup_Holdable( gentity_t *ent, gentity_t *other ) {
|
|
|
|
other->client->ps.stats[STAT_HOLDABLE_ITEM] = ent->item - bg_itemlist;
|
|
|
|
return 60;
|
|
}
|
|
|
|
|
|
//======================================================================
|
|
int Add_Ammo2 (gentity_t *ent, int ammoType, int count)
|
|
{
|
|
ent->client->ps.ammo[ammoType] += count;
|
|
|
|
if ( ent->client->ps.ammo[ammoType] > ammoData[ammoType].max )
|
|
{
|
|
ent->client->ps.ammo[ammoType] = ammoData[ammoType].max;
|
|
return qfalse;
|
|
}
|
|
return qtrue;
|
|
}
|
|
|
|
void Add_Ammo (gentity_t *ent, int weapon, int count)
|
|
{
|
|
Add_Ammo2(ent,weaponData[weapon].ammoIndex,count);
|
|
}
|
|
|
|
int Pickup_Ammo (gentity_t *ent, gentity_t *other)
|
|
{
|
|
int quantity;
|
|
|
|
if ( ent->count ) {
|
|
quantity = ent->count;
|
|
} else {
|
|
quantity = ent->item->quantity;
|
|
}
|
|
|
|
Add_Ammo2 (other, ent->item->giTag, quantity);
|
|
|
|
return 30;
|
|
}
|
|
|
|
//======================================================================
|
|
|
|
|
|
int Pickup_Weapon (gentity_t *ent, gentity_t *other) {
|
|
int quantity;
|
|
|
|
if (ent->count)
|
|
{
|
|
quantity = ent->count;
|
|
}
|
|
else
|
|
{
|
|
quantity = ent->item->quantity;
|
|
}
|
|
|
|
// The Scav rifle is a special case
|
|
if ( ent->item->giTag == WP_SCAVENGER_RIFLE )
|
|
{
|
|
switch (g_spskill->integer)
|
|
{
|
|
case 0: //easy
|
|
quantity = 40;
|
|
break;
|
|
case 1: //medium
|
|
quantity = 30;
|
|
break;
|
|
default:
|
|
case 2: //hard
|
|
quantity = 20;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// dropped items are always picked up
|
|
if (!quantity)
|
|
{
|
|
if ( ! (ent->flags & FL_DROPPED_ITEM ) ) {
|
|
quantity = 50;
|
|
}
|
|
}
|
|
|
|
// add the weapon
|
|
other->client->ps.stats[STAT_WEAPONS] |= ( 1 << ent->item->giTag );
|
|
|
|
// Give ammo
|
|
Add_Ammo( other, ent->item->giTag, quantity );
|
|
|
|
return 5;
|
|
}
|
|
|
|
|
|
//======================================================================
|
|
|
|
int ITM_AddHealth (gentity_t *ent, int count)
|
|
{
|
|
|
|
ent->health += count;
|
|
|
|
if (ent->health > ent->client->ps.stats[STAT_MAX_HEALTH]) // Past max health
|
|
{
|
|
ent->health = ent->client->ps.stats[STAT_MAX_HEALTH];
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
return qtrue;
|
|
|
|
}
|
|
|
|
int Pickup_Health (gentity_t *ent, gentity_t *other) {
|
|
int max;
|
|
int quantity;
|
|
|
|
max = other->client->ps.stats[STAT_MAX_HEALTH];
|
|
|
|
if ( ent->count ) {
|
|
quantity = ent->count;
|
|
} else {
|
|
quantity = ent->item->quantity;
|
|
}
|
|
|
|
other->health += quantity;
|
|
|
|
if (other->health > max ) {
|
|
other->health = max;
|
|
}
|
|
|
|
if ( ent->item->giTag == 100 ) { // mega health respawns slow
|
|
return 120;
|
|
}
|
|
|
|
return 30;
|
|
}
|
|
|
|
//======================================================================
|
|
|
|
int ITM_AddArmor (gentity_t *ent, int count)
|
|
{
|
|
|
|
ent->client->ps.stats[STAT_ARMOR] += count;
|
|
|
|
if (ent->client->ps.stats[STAT_ARMOR] > ent->client->ps.stats[STAT_MAX_HEALTH])
|
|
{
|
|
ent->client->ps.stats[STAT_ARMOR] = ent->client->ps.stats[STAT_MAX_HEALTH];
|
|
return qfalse;
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
int Pickup_Armor( gentity_t *ent, gentity_t *other ) {
|
|
other->client->ps.stats[STAT_ARMOR] += ent->item->quantity;
|
|
if ( other->client->ps.stats[STAT_ARMOR] > other->client->ps.stats[STAT_MAX_HEALTH] ) {
|
|
other->client->ps.stats[STAT_ARMOR] = other->client->ps.stats[STAT_MAX_HEALTH];
|
|
}
|
|
|
|
return 30;
|
|
}
|
|
|
|
//======================================================================
|
|
|
|
/*
|
|
===============
|
|
RespawnItem
|
|
===============
|
|
*/
|
|
void RespawnItem( gentity_t *ent ) {
|
|
}
|
|
|
|
|
|
/*
|
|
===============
|
|
Touch_Item
|
|
===============
|
|
*/
|
|
void Touch_Item (gentity_t *ent, gentity_t *other, trace_t *trace) {
|
|
int respawn;
|
|
|
|
if (!other->client)
|
|
return;
|
|
if (other->health < 1)
|
|
return; // dead people can't pickup
|
|
|
|
// Only monsters can pick it up
|
|
if ((ent->spawnflags & ITMSF_MONSTER) && (other->client->playerTeam == TEAM_STARFLEET))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Only star fleet can pick it up
|
|
if ((ent->spawnflags & ITMSF_TEAM) && (other->client->playerTeam != TEAM_STARFLEET))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Only player can pick it up
|
|
if (!(ent->spawnflags & ITMSF_TEAM) && !(ent->spawnflags & ITMSF_MONSTER))
|
|
{
|
|
if (!other->s.number == 0) // Not the player?
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// the same pickup rules are used for client side and server side
|
|
if ( !BG_CanItemBeGrabbed( &ent->s, &other->client->ps ) ) {
|
|
return;
|
|
}
|
|
|
|
if (!ent->item) { //not an item!
|
|
gi.Printf( "Touch_Item: %s is not an item!\n", ent->classname);
|
|
return;
|
|
}
|
|
// call the item-specific pickup function
|
|
switch( ent->item->giType )
|
|
{
|
|
case IT_WEAPON:
|
|
respawn = Pickup_Weapon(ent, other);
|
|
break;
|
|
case IT_AMMO:
|
|
respawn = Pickup_Ammo(ent, other);
|
|
break;
|
|
case IT_ARMOR:
|
|
respawn = Pickup_Armor(ent, other);
|
|
break;
|
|
case IT_HEALTH:
|
|
respawn = Pickup_Health(ent, other);
|
|
break;
|
|
case IT_HOLDABLE:
|
|
respawn = Pickup_Holdable(ent, other);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
if ( !respawn )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// play the normal pickup sound
|
|
G_AddEvent( other, EV_ITEM_PICKUP, ent->s.modelindex );
|
|
|
|
// fire item targets
|
|
G_UseTargets (ent, other);
|
|
|
|
// wait of -1 will not respawn
|
|
// if ( ent->wait == -1 )
|
|
{
|
|
ent->svFlags |= SVF_NOCLIENT;
|
|
ent->s.eFlags |= EF_NODRAW;
|
|
ent->contents = 0;
|
|
ent->unlinkAfterEvent = qtrue;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
//======================================================================
|
|
|
|
/*
|
|
================
|
|
LaunchItem
|
|
|
|
Spawns an item and tosses it forward
|
|
================
|
|
*/
|
|
gentity_t *LaunchItem( gitem_t *item, vec3_t origin, vec3_t velocity, char *target ) {
|
|
gentity_t *dropped;
|
|
|
|
dropped = G_Spawn();
|
|
|
|
dropped->s.eType = ET_ITEM;
|
|
dropped->s.modelindex = item - bg_itemlist; // store item number in modelindex
|
|
dropped->s.modelindex2 = 1; // This is non-zero is it's a dropped item
|
|
|
|
dropped->classname = item->classname;
|
|
dropped->item = item;
|
|
VectorSet (dropped->mins, -ITEM_RADIUS, -ITEM_RADIUS, -ITEM_RADIUS);
|
|
VectorSet (dropped->maxs, ITEM_RADIUS, ITEM_RADIUS, ITEM_RADIUS);
|
|
dropped->contents = CONTENTS_TRIGGER|CONTENTS_ITEM;//CONTENTS_TRIGGER;//not CONTENTS_BODY for dropped items, don't need to ID them
|
|
|
|
if ( target && target[0] )
|
|
{
|
|
dropped->target = G_NewString( target );
|
|
}
|
|
else
|
|
{
|
|
// if not targeting something, auto-remove after 30 seconds
|
|
dropped->e_ThinkFunc = thinkF_G_FreeEntity;
|
|
dropped->nextthink = level.time + 30000;
|
|
}
|
|
|
|
dropped->e_TouchFunc = touchF_Touch_Item;
|
|
|
|
G_SetOrigin( dropped, origin );
|
|
dropped->s.pos.trType = TR_GRAVITY;
|
|
dropped->s.pos.trTime = level.time;
|
|
VectorCopy( velocity, dropped->s.pos.trDelta );
|
|
|
|
dropped->s.eFlags |= EF_BOUNCE_HALF;
|
|
|
|
dropped->flags = FL_DROPPED_ITEM;
|
|
|
|
gi.linkentity (dropped);
|
|
|
|
return dropped;
|
|
}
|
|
|
|
/*
|
|
================
|
|
Drop_Item
|
|
|
|
Spawns an item and tosses it forward
|
|
================
|
|
*/
|
|
gentity_t *Drop_Item( gentity_t *ent, gitem_t *item, float angle, qboolean copytarget ) {
|
|
vec3_t velocity;
|
|
vec3_t angles;
|
|
|
|
VectorCopy( ent->s.apos.trBase, angles );
|
|
angles[YAW] += angle;
|
|
angles[PITCH] = 0; // always forward
|
|
|
|
AngleVectors( angles, velocity, NULL, NULL );
|
|
VectorScale( velocity, 150, velocity );
|
|
velocity[2] += 200 + crandom() * 50;
|
|
|
|
if ( copytarget )
|
|
{
|
|
return LaunchItem( item, ent->s.pos.trBase, velocity, ent->opentarget );
|
|
}
|
|
else
|
|
{
|
|
return LaunchItem( item, ent->s.pos.trBase, velocity, NULL );
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
Use_Item
|
|
|
|
Respawn the item
|
|
================
|
|
*/
|
|
void Use_Item( gentity_t *ent, gentity_t *other, gentity_t *activator )
|
|
{
|
|
if ( ent->spawnflags & 32 ) // invisible
|
|
{
|
|
// If it was invisible, first use makes it visible....
|
|
ent->s.eFlags &= ~EF_NODRAW;
|
|
ent->contents = CONTENTS_TRIGGER|CONTENTS_ITEM;
|
|
|
|
ent->spawnflags &= ~32;
|
|
return;
|
|
}
|
|
|
|
if (ent->behaviorSet[BSET_USE])
|
|
{
|
|
G_ActivateBehavior(ent,BSET_USE);
|
|
}
|
|
|
|
RespawnItem( ent );
|
|
}
|
|
|
|
//======================================================================
|
|
|
|
/*
|
|
================
|
|
FinishSpawningItem
|
|
|
|
Traces down to find where an item should rest, instead of letting them
|
|
free fall from their spawn points
|
|
================
|
|
*/
|
|
void FinishSpawningItem( gentity_t *ent ) {
|
|
trace_t tr;
|
|
vec3_t dest;
|
|
gitem_t *item;
|
|
int itemNum;
|
|
|
|
itemNum=1;
|
|
for ( item = bg_itemlist + 1 ; item->classname ; item++,itemNum++)
|
|
{
|
|
if (!strcmp(item->classname,ent->classname))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Set bounding box for item
|
|
VectorSet( ent->mins, item->mins[0],item->mins[1] ,item->mins[2]);
|
|
VectorSet( ent->maxs, item->maxs[0],item->maxs[1] ,item->maxs[2]);
|
|
|
|
if ((!ent->mins[0] && !ent->mins[1] && !ent->mins[2]) &&
|
|
(!ent->maxs[0] && !ent->maxs[1] && !ent->maxs[2]))
|
|
{
|
|
VectorSet (ent->mins, -ITEM_RADIUS, -ITEM_RADIUS, -ITEM_RADIUS);
|
|
VectorSet (ent->maxs, ITEM_RADIUS, ITEM_RADIUS, ITEM_RADIUS);
|
|
}
|
|
|
|
if ((item->quantity) && (item->giType == IT_AMMO))
|
|
{
|
|
ent->count = item->quantity;
|
|
}
|
|
|
|
// Set crystal ammo amount based on skill level
|
|
if ((itemNum == ITM_AMMO_CRYSTAL_BORG) ||
|
|
(itemNum == ITM_AMMO_CRYSTAL_DN) ||
|
|
(itemNum == ITM_AMMO_CRYSTAL_FORGE) ||
|
|
(itemNum == ITM_AMMO_CRYSTAL_SCAVENGER) ||
|
|
(itemNum == ITM_AMMO_CRYSTAL_STASIS))
|
|
{
|
|
CrystalAmmoSettings(ent);
|
|
}
|
|
|
|
ent->s.eType = ET_ITEM;
|
|
ent->s.modelindex = ent->item - bg_itemlist; // store item number in modelindex
|
|
ent->s.modelindex2 = 0; // zero indicates this isn't a dropped item
|
|
|
|
ent->contents = CONTENTS_TRIGGER|CONTENTS_ITEM;//CONTENTS_BODY;//CONTENTS_TRIGGER|
|
|
ent->e_TouchFunc = touchF_Touch_Item;
|
|
// useing an item causes it to respawn
|
|
ent->e_UseFunc = useF_Use_Item;
|
|
|
|
// Hang in air?
|
|
if ( ent->spawnflags & ITMSF_SUSPEND)
|
|
{
|
|
// suspended
|
|
G_SetOrigin( ent, ent->s.origin );
|
|
}
|
|
else
|
|
{
|
|
// drop to floor
|
|
VectorSet( dest, ent->s.origin[0], ent->s.origin[1], ent->s.origin[2] - 4096 );
|
|
gi.trace( &tr, ent->s.origin, ent->mins, ent->maxs, dest, ent->s.number, MASK_SOLID );
|
|
if ( tr.startsolid )
|
|
{
|
|
gi.Printf (S_COLOR_RED"FinishSpawningItem: removing %s startsolid at %s\n", ent->classname, vtos(ent->s.origin));
|
|
assert( 0 && "item starting in solid");
|
|
#ifndef FINAL_BUILD
|
|
G_Error("FinishSpawningItem: removing %s startsolid at %s\n", ent->classname, vtos(ent->s.origin));
|
|
#endif
|
|
G_FreeEntity( ent );
|
|
return;
|
|
}
|
|
|
|
// allow to ride movers
|
|
ent->s.groundEntityNum = tr.entityNum;
|
|
|
|
G_SetOrigin( ent, tr.endpos );
|
|
}
|
|
|
|
/* ? don't need this
|
|
// team slaves and targeted items aren't present at start
|
|
if ( ( ent->flags & FL_TEAMSLAVE ) || ent->targetname ) {
|
|
ent->s.eFlags |= EF_NODRAW;
|
|
ent->contents = 0;
|
|
return;
|
|
}
|
|
*/
|
|
if ( ent->spawnflags & ITMSF_INVISIBLE ) // invisible
|
|
{
|
|
ent->s.eFlags |= EF_NODRAW;
|
|
ent->contents = 0;
|
|
}
|
|
|
|
if ( ent->spawnflags & ITMSF_NOTSOLID ) // not solid
|
|
{
|
|
ent->contents = 0;
|
|
}
|
|
|
|
gi.linkentity (ent);
|
|
}
|
|
|
|
|
|
qboolean itemRegistered[MAX_ITEMS];
|
|
|
|
|
|
/*
|
|
==============
|
|
ClearRegisteredItems
|
|
==============
|
|
*/
|
|
void ClearRegisteredItems( void ) {
|
|
memset( itemRegistered, 0, sizeof( itemRegistered ) );
|
|
|
|
// players always start with the base weapon
|
|
RegisterItem( FindItemForWeapon( WP_PHASER ) ); //these are given in g_client, ClientSpawn()
|
|
RegisterItem( FindItemForWeapon( WP_COMPRESSION_RIFLE ) );
|
|
|
|
extern void Player_CacheFromPrevLevel(void);//g_client.cpp
|
|
Player_CacheFromPrevLevel(); //reads from transition carry-over;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
RegisterItem
|
|
|
|
The item will be added to the precache list
|
|
===============
|
|
*/
|
|
void RegisterItem( gitem_t *item ) {
|
|
if ( !item ) {
|
|
G_Error( "RegisterItem: NULL" );
|
|
}
|
|
itemRegistered[ item - bg_itemlist ] = qtrue;
|
|
}
|
|
|
|
|
|
/*
|
|
===============
|
|
SaveRegisteredItems
|
|
|
|
Write the needed items to a config string
|
|
so the client will know which ones to precache
|
|
===============
|
|
*/
|
|
void SaveRegisteredItems( void ) {
|
|
char string[MAX_ITEMS+1];
|
|
int i;
|
|
int count;
|
|
|
|
count = 0;
|
|
for ( i = 0 ; i < bg_numItems ; i++ ) {
|
|
if ( itemRegistered[i] ) {
|
|
count++;
|
|
string[i] = '1';
|
|
} else {
|
|
string[i] = '0';
|
|
}
|
|
}
|
|
string[ bg_numItems ] = 0;
|
|
|
|
gi.Printf( "%i items registered\n", count );
|
|
gi.SetConfigstring(CS_ITEMS, string);
|
|
}
|
|
|
|
|
|
/*
|
|
============
|
|
G_SpawnItem
|
|
|
|
Sets the clipping size and plants the object on the floor.
|
|
|
|
Items can't be immediately dropped to floor, because they might
|
|
be on an entity that hasn't spawned yet.
|
|
============
|
|
*/
|
|
void G_SpawnItem (gentity_t *ent, gitem_t *item) {
|
|
G_SpawnFloat( "random", "0", &ent->random );
|
|
G_SpawnFloat( "wait", "0", &ent->wait );
|
|
|
|
RegisterItem( item );
|
|
ent->item = item;
|
|
|
|
// some movers spawn on the second frame, so delay item
|
|
// spawns until the third frame so they can ride trains
|
|
ent->nextthink = level.time + FRAMETIME * 2;
|
|
ent->e_ThinkFunc = thinkF_FinishSpawningItem;
|
|
|
|
ent->physicsBounce = 0.50; // items are bouncy
|
|
|
|
// Set a default infoString text color
|
|
VectorSet( ent->startRGBA, 0.9f, 0.7f, 0.0f ); // yellow
|
|
ent->infoString = item->pickup_name;
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
G_BounceItem
|
|
|
|
================
|
|
*/
|
|
void G_BounceItem( gentity_t *ent, trace_t *trace ) {
|
|
vec3_t velocity;
|
|
float dot;
|
|
int hitTime;
|
|
|
|
// reflect the velocity on the trace plane
|
|
hitTime = level.previousTime + ( level.time - level.previousTime ) * trace->fraction;
|
|
EvaluateTrajectoryDelta( &ent->s.pos, hitTime, velocity );
|
|
dot = DotProduct( velocity, trace->plane.normal );
|
|
VectorMA( velocity, -2*dot, trace->plane.normal, ent->s.pos.trDelta );
|
|
|
|
// cut the velocity to keep from bouncing forever
|
|
VectorScale( ent->s.pos.trDelta, ent->physicsBounce, ent->s.pos.trDelta );
|
|
|
|
// check for stop
|
|
if ( trace->plane.normal[2] > 0 && ent->s.pos.trDelta[2] < 40 ) {
|
|
G_SetOrigin( ent, trace->endpos );
|
|
ent->s.groundEntityNum = trace->entityNum;
|
|
return;
|
|
}
|
|
|
|
VectorAdd( ent->currentOrigin, trace->plane.normal, ent->currentOrigin);
|
|
VectorCopy( ent->currentOrigin, ent->s.pos.trBase );
|
|
ent->s.pos.trTime = level.time;
|
|
}
|
|
|
|
|
|
/*
|
|
================
|
|
G_RunItem
|
|
|
|
================
|
|
*/
|
|
void G_RunItem( gentity_t *ent ) {
|
|
vec3_t origin;
|
|
trace_t tr;
|
|
int contents;
|
|
int mask;
|
|
|
|
// if groundentity has been set to -1, it may have been pushed off an edge
|
|
if ( ent->s.groundEntityNum == -1 ) {
|
|
if ( ent->s.pos.trType != TR_GRAVITY ) {
|
|
ent->s.pos.trType = TR_GRAVITY;
|
|
ent->s.pos.trTime = level.time;
|
|
}
|
|
}
|
|
|
|
if ( ent->s.pos.trType == TR_STATIONARY ) {
|
|
// check think function
|
|
G_RunThink( ent );
|
|
return;
|
|
}
|
|
|
|
// get current position
|
|
EvaluateTrajectory( &ent->s.pos, level.time, origin );
|
|
|
|
// trace a line from the previous position to the current position
|
|
if ( ent->clipmask ) {
|
|
mask = ent->clipmask;
|
|
} else {
|
|
mask = MASK_SOLID;
|
|
}
|
|
gi.trace( &tr, ent->currentOrigin, ent->mins, ent->maxs, origin,
|
|
ent->owner ? ent->owner->s.number : ENTITYNUM_NONE, mask );
|
|
|
|
VectorCopy( tr.endpos, ent->currentOrigin );
|
|
|
|
if ( tr.startsolid ) {
|
|
tr.fraction = 0;
|
|
}
|
|
|
|
gi.linkentity( ent ); // FIXME: avoid this for stationary?
|
|
|
|
// check think function
|
|
G_RunThink( ent );
|
|
|
|
if ( tr.fraction == 1 ) {
|
|
return;
|
|
}
|
|
|
|
// if it is in a nodrop volume, remove it
|
|
contents = gi.pointcontents( ent->currentOrigin, -1 );
|
|
if ( contents & CONTENTS_NODROP )
|
|
{
|
|
G_FreeEntity( ent );
|
|
return;
|
|
}
|
|
|
|
G_BounceItem( ent, &tr );
|
|
}
|
|
|