mirror of
https://github.com/yquake2/rogue.git
synced 2024-11-28 23:12:07 +00:00
3943 lines
72 KiB
C
3943 lines
72 KiB
C
/*
|
|
* =======================================================================
|
|
*
|
|
* Level functions. Platforms, buttons, dooors and so on.
|
|
*
|
|
* =======================================================================
|
|
*/
|
|
|
|
#include "header/local.h"
|
|
|
|
#define PLAT_LOW_TRIGGER 1
|
|
#define PLAT2_TOGGLE 2
|
|
#define PLAT2_TOP 4
|
|
#define PLAT2_TRIGGER_TOP 8
|
|
#define PLAT2_TRIGGER_BOTTOM 16
|
|
#define PLAT2_BOX_LIFT 32
|
|
|
|
#define STATE_TOP 0
|
|
#define STATE_BOTTOM 1
|
|
#define STATE_UP 2
|
|
#define STATE_DOWN 3
|
|
|
|
#define DOOR_START_OPEN 1
|
|
#define DOOR_REVERSE 2
|
|
#define DOOR_CRUSHER 4
|
|
#define DOOR_NOMONSTER 8
|
|
#define DOOR_TOGGLE 32
|
|
#define DOOR_X_AXIS 64
|
|
#define DOOR_Y_AXIS 128
|
|
#define DOOR_INACTIVE 8192
|
|
|
|
#define AccelerationDistance(target, rate) (target * ((target / rate) + 1) / 2)
|
|
|
|
#define PLAT2_CALLED 1
|
|
#define PLAT2_MOVING 2
|
|
#define PLAT2_WAITING 4
|
|
|
|
#define TRAIN_START_ON 1
|
|
#define TRAIN_TOGGLE 2
|
|
#define TRAIN_BLOCK_STOPS 4
|
|
|
|
#define SECRET_ALWAYS_SHOOT 1
|
|
#define SECRET_1ST_LEFT 2
|
|
#define SECRET_1ST_DOWN 4
|
|
|
|
void door_secret_move1(edict_t *self);
|
|
void door_secret_move2(edict_t *self);
|
|
void door_secret_move3(edict_t *self);
|
|
void door_secret_move4(edict_t *self);
|
|
void door_secret_move5(edict_t *self);
|
|
void door_secret_move6(edict_t *self);
|
|
void door_secret_done(edict_t *self);
|
|
|
|
void train_next(edict_t *self);
|
|
void door_go_down(edict_t *self);
|
|
void plat2_go_down(edict_t *ent);
|
|
void plat2_go_up(edict_t *ent);
|
|
void plat2_spawn_danger_area(edict_t *ent);
|
|
void plat2_kill_danger_area(edict_t *ent);
|
|
void Think_AccelMove(edict_t *ent);
|
|
void plat_go_down(edict_t *ent);
|
|
|
|
/*
|
|
* =========================================================
|
|
*
|
|
* PLATS
|
|
*
|
|
* movement options:
|
|
*
|
|
* linear
|
|
* smooth start, hard stop
|
|
* smooth start, smooth stop
|
|
*
|
|
* start
|
|
* end
|
|
* acceleration
|
|
* speed
|
|
* deceleration
|
|
* begin sound
|
|
* end sound
|
|
* target fired when reaching end
|
|
* wait at end
|
|
*
|
|
* object characteristics that use move segments
|
|
* ---------------------------------------------
|
|
* movetype_push, or movetype_stop
|
|
* action when touched
|
|
* action when blocked
|
|
* action when used
|
|
* disabled?
|
|
* auto trigger spawning
|
|
*
|
|
*
|
|
* =========================================================
|
|
*/
|
|
|
|
/* Support routines for movement (changes in origin using velocity) */
|
|
|
|
void
|
|
Move_Done(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorClear(ent->velocity);
|
|
ent->moveinfo.endfunc(ent);
|
|
}
|
|
|
|
void
|
|
Move_Final(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ent->moveinfo.remaining_distance == 0)
|
|
{
|
|
Move_Done(ent);
|
|
return;
|
|
}
|
|
|
|
VectorScale(ent->moveinfo.dir,
|
|
ent->moveinfo.remaining_distance / FRAMETIME,
|
|
ent->velocity);
|
|
|
|
ent->think = Move_Done;
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
void
|
|
Move_Begin(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
float frames;
|
|
|
|
if ((ent->moveinfo.speed * FRAMETIME) >= ent->moveinfo.remaining_distance)
|
|
{
|
|
Move_Final(ent);
|
|
return;
|
|
}
|
|
|
|
VectorScale(ent->moveinfo.dir, ent->moveinfo.speed, ent->velocity);
|
|
frames = floor( (ent->moveinfo.remaining_distance /
|
|
ent->moveinfo.speed) / FRAMETIME);
|
|
ent->moveinfo.remaining_distance -= frames * ent->moveinfo.speed * FRAMETIME;
|
|
ent->nextthink = level.time + (frames * FRAMETIME);
|
|
ent->think = Move_Final;
|
|
}
|
|
|
|
void
|
|
Move_Calc(edict_t *ent, vec3_t dest, void (*func)(edict_t *))
|
|
{
|
|
if (!ent || !func)
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorClear(ent->velocity);
|
|
VectorSubtract(dest, ent->s.origin, ent->moveinfo.dir);
|
|
ent->moveinfo.remaining_distance = VectorNormalize(ent->moveinfo.dir);
|
|
ent->moveinfo.endfunc = func;
|
|
|
|
if ((ent->moveinfo.speed == ent->moveinfo.accel) &&
|
|
(ent->moveinfo.speed == ent->moveinfo.decel))
|
|
{
|
|
if (level.current_entity ==
|
|
((ent->flags & FL_TEAMSLAVE) ? ent->teammaster : ent))
|
|
{
|
|
Move_Begin(ent);
|
|
}
|
|
else
|
|
{
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
ent->think = Move_Begin;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* accelerative */
|
|
ent->moveinfo.current_speed = 0;
|
|
ent->think = Think_AccelMove;
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
}
|
|
}
|
|
|
|
/* Support routines for angular movement
|
|
(changes in angle using avelocity) */
|
|
void
|
|
AngleMove_Done(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorClear(ent->avelocity);
|
|
ent->moveinfo.endfunc(ent);
|
|
}
|
|
|
|
void
|
|
AngleMove_Final(edict_t *ent)
|
|
{
|
|
vec3_t move;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ent->moveinfo.state == STATE_UP)
|
|
{
|
|
VectorSubtract(ent->moveinfo.end_angles, ent->s.angles, move);
|
|
}
|
|
else
|
|
{
|
|
VectorSubtract(ent->moveinfo.start_angles, ent->s.angles, move);
|
|
}
|
|
|
|
if (VectorCompare(move, vec3_origin))
|
|
{
|
|
AngleMove_Done(ent);
|
|
return;
|
|
}
|
|
|
|
VectorScale(move, 1.0 / FRAMETIME, ent->avelocity);
|
|
|
|
ent->think = AngleMove_Done;
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
void
|
|
AngleMove_Begin(edict_t *ent)
|
|
{
|
|
vec3_t destdelta;
|
|
float len;
|
|
float traveltime;
|
|
float frames;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* accelerate as needed */
|
|
if (ent->moveinfo.speed < ent->speed)
|
|
{
|
|
ent->moveinfo.speed += ent->accel;
|
|
|
|
if (ent->moveinfo.speed > ent->speed)
|
|
{
|
|
ent->moveinfo.speed = ent->speed;
|
|
}
|
|
}
|
|
|
|
/* set destdelta to the vector needed to move */
|
|
if (ent->moveinfo.state == STATE_UP)
|
|
{
|
|
VectorSubtract(ent->moveinfo.end_angles, ent->s.angles, destdelta);
|
|
}
|
|
else
|
|
{
|
|
VectorSubtract(ent->moveinfo.start_angles, ent->s.angles, destdelta);
|
|
}
|
|
|
|
/* calculate length of vector */
|
|
len = VectorLength(destdelta);
|
|
|
|
/* divide by speed to get time to reach dest */
|
|
traveltime = len / ent->moveinfo.speed;
|
|
|
|
if (traveltime < FRAMETIME)
|
|
{
|
|
AngleMove_Final(ent);
|
|
return;
|
|
}
|
|
|
|
frames = floor(traveltime / FRAMETIME);
|
|
|
|
/* scale the destdelta vector by the time spent traveling to get velocity */
|
|
VectorScale(destdelta, 1.0 / traveltime, ent->avelocity);
|
|
|
|
/* if we're done accelerating, act as a normal rotation */
|
|
if (ent->moveinfo.speed >= ent->speed)
|
|
{
|
|
/* set nextthink to trigger a think when dest is reached */
|
|
ent->nextthink = level.time + frames * FRAMETIME;
|
|
ent->think = AngleMove_Final;
|
|
}
|
|
else
|
|
{
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
ent->think = AngleMove_Begin;
|
|
}
|
|
}
|
|
|
|
void
|
|
AngleMove_Calc(edict_t *ent, void (*func)(edict_t *))
|
|
{
|
|
if (!ent || !func)
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorClear(ent->avelocity);
|
|
ent->moveinfo.endfunc = func;
|
|
|
|
/* if we're supposed to accelerate, this will
|
|
tell anglemove_begin to do so */
|
|
if (ent->accel != ent->speed)
|
|
{
|
|
ent->moveinfo.speed = 0;
|
|
}
|
|
|
|
if (level.current_entity ==
|
|
((ent->flags & FL_TEAMSLAVE) ? ent->teammaster : ent))
|
|
{
|
|
AngleMove_Begin(ent);
|
|
}
|
|
else
|
|
{
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
ent->think = AngleMove_Begin;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* The team has completed a frame of movement, so
|
|
* change the speed for the next frame
|
|
*/
|
|
|
|
void
|
|
plat_CalcAcceleratedMove(moveinfo_t *moveinfo)
|
|
{
|
|
float accel_dist;
|
|
float decel_dist;
|
|
|
|
if (!moveinfo)
|
|
{
|
|
return;
|
|
}
|
|
|
|
moveinfo->move_speed = moveinfo->speed;
|
|
|
|
if (moveinfo->remaining_distance < moveinfo->accel)
|
|
{
|
|
moveinfo->current_speed = moveinfo->remaining_distance;
|
|
return;
|
|
}
|
|
|
|
accel_dist = AccelerationDistance(moveinfo->speed, moveinfo->accel);
|
|
decel_dist = AccelerationDistance(moveinfo->speed, moveinfo->decel);
|
|
|
|
if ((moveinfo->remaining_distance - accel_dist - decel_dist) < 0)
|
|
{
|
|
float f;
|
|
|
|
f = (moveinfo->accel + moveinfo->decel) / (moveinfo->accel * moveinfo->decel);
|
|
moveinfo->move_speed = (-2 + sqrt(4 - 4 * f * (-2 * moveinfo->remaining_distance))) / (2 * f);
|
|
decel_dist = AccelerationDistance(moveinfo->move_speed, moveinfo->decel);
|
|
}
|
|
|
|
moveinfo->decel_distance = decel_dist;
|
|
}
|
|
|
|
void
|
|
plat_Accelerate(moveinfo_t *moveinfo)
|
|
{
|
|
if (!moveinfo)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* are we decelerating? */
|
|
if (moveinfo->remaining_distance <= moveinfo->decel_distance)
|
|
{
|
|
if (moveinfo->remaining_distance < moveinfo->decel_distance)
|
|
{
|
|
if (moveinfo->next_speed)
|
|
{
|
|
moveinfo->current_speed = moveinfo->next_speed;
|
|
moveinfo->next_speed = 0;
|
|
return;
|
|
}
|
|
|
|
if (moveinfo->current_speed > moveinfo->decel)
|
|
{
|
|
moveinfo->current_speed -= moveinfo->decel;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/* are we at full speed and need to start decelerating during this move? */
|
|
if (moveinfo->current_speed == moveinfo->move_speed)
|
|
{
|
|
if ((moveinfo->remaining_distance - moveinfo->current_speed) <
|
|
moveinfo->decel_distance)
|
|
{
|
|
float p1_distance;
|
|
float p2_distance;
|
|
float distance;
|
|
|
|
p1_distance = moveinfo->remaining_distance -
|
|
moveinfo->decel_distance;
|
|
p2_distance = moveinfo->move_speed *
|
|
(1.0 - (p1_distance / moveinfo->move_speed));
|
|
distance = p1_distance + p2_distance;
|
|
moveinfo->current_speed = moveinfo->move_speed;
|
|
moveinfo->next_speed = moveinfo->move_speed - moveinfo->decel *
|
|
(p2_distance / distance);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* are we accelerating? */
|
|
if (moveinfo->current_speed < moveinfo->speed)
|
|
{
|
|
float old_speed;
|
|
float p1_distance;
|
|
float p1_speed;
|
|
float p2_distance;
|
|
float distance;
|
|
|
|
old_speed = moveinfo->current_speed;
|
|
|
|
/* figure simple acceleration up to move_speed */
|
|
moveinfo->current_speed += moveinfo->accel;
|
|
|
|
if (moveinfo->current_speed > moveinfo->speed)
|
|
{
|
|
moveinfo->current_speed = moveinfo->speed;
|
|
}
|
|
|
|
/* are we accelerating throughout this entire move? */
|
|
if ((moveinfo->remaining_distance - moveinfo->current_speed) >=
|
|
moveinfo->decel_distance)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* during this move we will accelrate from current_speed to move_speed
|
|
and cross over the decel_distance; figure the average speed for the
|
|
entire move */
|
|
p1_distance = moveinfo->remaining_distance - moveinfo->decel_distance;
|
|
p1_speed = (old_speed + moveinfo->move_speed) / 2.0;
|
|
p2_distance = moveinfo->move_speed * (1.0 - (p1_distance / p1_speed));
|
|
distance = p1_distance + p2_distance;
|
|
moveinfo->current_speed = (p1_speed * (p1_distance /
|
|
distance)) + (moveinfo->move_speed * (p2_distance / distance));
|
|
moveinfo->next_speed = moveinfo->move_speed - moveinfo->decel *
|
|
(p2_distance / distance);
|
|
return;
|
|
}
|
|
|
|
/* we are at constant velocity (move_speed) */
|
|
return;
|
|
}
|
|
|
|
void
|
|
Think_AccelMove(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ent->moveinfo.remaining_distance -= ent->moveinfo.current_speed;
|
|
plat_CalcAcceleratedMove(&ent->moveinfo);
|
|
plat_Accelerate(&ent->moveinfo);
|
|
|
|
/* will the entire move complete on next frame? */
|
|
if (ent->moveinfo.remaining_distance <= ent->moveinfo.current_speed)
|
|
{
|
|
Move_Final(ent);
|
|
return;
|
|
}
|
|
|
|
VectorScale(ent->moveinfo.dir, ent->moveinfo.current_speed * 10,
|
|
ent->velocity);
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
ent->think = Think_AccelMove;
|
|
}
|
|
|
|
void
|
|
plat_hit_top(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(ent->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (ent->moveinfo.sound_end)
|
|
{
|
|
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE, ent->moveinfo.sound_end,
|
|
1, ATTN_STATIC, 0);
|
|
}
|
|
|
|
ent->s.sound = 0;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_TOP;
|
|
|
|
ent->think = plat_go_down;
|
|
ent->nextthink = level.time + 3;
|
|
}
|
|
|
|
void
|
|
plat_hit_bottom(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(ent->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (ent->moveinfo.sound_end)
|
|
{
|
|
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
ent->moveinfo.sound_end, 1, ATTN_STATIC, 0);
|
|
}
|
|
|
|
ent->s.sound = 0;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_BOTTOM;
|
|
|
|
plat2_kill_danger_area(ent);
|
|
}
|
|
|
|
void
|
|
plat_go_down(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(ent->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (ent->moveinfo.sound_start)
|
|
{
|
|
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
ent->moveinfo.sound_start, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
ent->s.sound = ent->moveinfo.sound_middle;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_DOWN;
|
|
Move_Calc(ent, ent->moveinfo.end_origin, plat_hit_bottom);
|
|
}
|
|
|
|
void
|
|
plat_go_up(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(ent->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (ent->moveinfo.sound_start)
|
|
{
|
|
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
ent->moveinfo.sound_start, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
ent->s.sound = ent->moveinfo.sound_middle;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_UP;
|
|
Move_Calc(ent, ent->moveinfo.start_origin, plat_hit_top);
|
|
|
|
plat2_spawn_danger_area(ent);
|
|
}
|
|
|
|
void
|
|
plat_blocked(edict_t *self, edict_t *other)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(other->svflags & SVF_MONSTER) && (!other->client))
|
|
{
|
|
/* give it a chance to go away on it's own terms (like gibs) */
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, 100000, 1, 0, MOD_CRUSH);
|
|
|
|
/* if it's still there, nuke it */
|
|
if (other->inuse)
|
|
{
|
|
/* Hack for entity without it's origin near the model */
|
|
VectorMA(other->absmin, 0.5, other->size, other->s.origin);
|
|
BecomeExplosion1(other);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/* gib dead things */
|
|
if (other->health < 1)
|
|
{
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, 100, 1, 0, MOD_CRUSH);
|
|
}
|
|
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
|
|
|
|
if (self->moveinfo.state == STATE_UP)
|
|
{
|
|
plat_go_down(self);
|
|
}
|
|
else if (self->moveinfo.state == STATE_DOWN)
|
|
{
|
|
plat_go_up(self);
|
|
}
|
|
}
|
|
|
|
void
|
|
Use_Plat(edict_t *ent, edict_t *other, edict_t *activator /* unused */)
|
|
{
|
|
if (!ent || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* if a monster is using us, then allow the activity when stopped. */
|
|
if (other->svflags & SVF_MONSTER)
|
|
{
|
|
if (ent->moveinfo.state == STATE_TOP)
|
|
{
|
|
plat_go_down(ent);
|
|
}
|
|
else if (ent->moveinfo.state == STATE_BOTTOM)
|
|
{
|
|
plat_go_up(ent);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (ent->think)
|
|
{
|
|
return; /* already down */
|
|
}
|
|
|
|
plat_go_down(ent);
|
|
}
|
|
|
|
void
|
|
Touch_Plat_Center(edict_t *ent, edict_t *other, cplane_t *plane /* unsed */,
|
|
csurface_t *surf /* unused */)
|
|
{
|
|
if (!ent || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!other->client)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (other->health <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ent = ent->enemy; /* now point at the plat, not the trigger */
|
|
|
|
if (ent->moveinfo.state == STATE_BOTTOM)
|
|
{
|
|
plat_go_up(ent);
|
|
}
|
|
else if (ent->moveinfo.state == STATE_TOP)
|
|
{
|
|
ent->nextthink = level.time + 1; /* the player is still on the plat, so delay going down */
|
|
}
|
|
}
|
|
|
|
edict_t *
|
|
plat_spawn_inside_trigger(edict_t *ent)
|
|
{
|
|
edict_t *trigger;
|
|
vec3_t tmin, tmax;
|
|
|
|
if (!ent)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* middle trigger */
|
|
trigger = G_Spawn();
|
|
trigger->touch = Touch_Plat_Center;
|
|
trigger->movetype = MOVETYPE_NONE;
|
|
trigger->solid = SOLID_TRIGGER;
|
|
trigger->enemy = ent;
|
|
|
|
tmin[0] = ent->mins[0] + 25;
|
|
tmin[1] = ent->mins[1] + 25;
|
|
|
|
tmax[0] = ent->maxs[0] - 25;
|
|
tmax[1] = ent->maxs[1] - 25;
|
|
tmax[2] = ent->maxs[2] + 8;
|
|
|
|
tmin[2] = tmax[2] - (ent->pos1[2] - ent->pos2[2] + st.lip);
|
|
|
|
if (ent->spawnflags & PLAT_LOW_TRIGGER)
|
|
{
|
|
tmax[2] = tmin[2] + 8;
|
|
}
|
|
|
|
if (tmax[0] - tmin[0] <= 0)
|
|
{
|
|
tmin[0] = (ent->mins[0] + ent->maxs[0]) * 0.5;
|
|
tmax[0] = tmin[0] + 1;
|
|
}
|
|
|
|
if (tmax[1] - tmin[1] <= 0)
|
|
{
|
|
tmin[1] = (ent->mins[1] + ent->maxs[1]) * 0.5;
|
|
tmax[1] = tmin[1] + 1;
|
|
}
|
|
|
|
VectorCopy(tmin, trigger->mins);
|
|
VectorCopy(tmax, trigger->maxs);
|
|
|
|
gi.linkentity(trigger);
|
|
|
|
return trigger;
|
|
}
|
|
|
|
/*
|
|
* QUAKED func_plat (0 .5 .8) ? PLAT_LOW_TRIGGER
|
|
*
|
|
* speed -> default 150
|
|
*
|
|
* Plats are always drawn in the extended position,
|
|
* so they will light correctly.
|
|
*
|
|
* If the plat is the target of another trigger or button,
|
|
* it will start out disabled in the extended position until
|
|
* it is trigger, when it will lower and become a normal plat.
|
|
*
|
|
* "speed" overrides default 200.
|
|
* "accel" overrides default 500
|
|
* "lip" overrides default 8 pixel lip
|
|
*
|
|
* If the "height" key is set, that will determine the amount
|
|
* the plat moves, instead of being implicitly determoveinfoned
|
|
* by the model's height.
|
|
*
|
|
* Set "sounds" to one of the following:
|
|
* 1) base fast
|
|
* 2) chain slow
|
|
*/
|
|
void
|
|
SP_func_plat(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorClear(ent->s.angles);
|
|
ent->solid = SOLID_BSP;
|
|
ent->movetype = MOVETYPE_PUSH;
|
|
|
|
gi.setmodel(ent, ent->model);
|
|
|
|
ent->blocked = plat_blocked;
|
|
|
|
if (!ent->speed)
|
|
{
|
|
ent->speed = 20;
|
|
}
|
|
else
|
|
{
|
|
ent->speed *= 0.1;
|
|
}
|
|
|
|
if (!ent->accel)
|
|
{
|
|
ent->accel = 5;
|
|
}
|
|
else
|
|
{
|
|
ent->accel *= 0.1;
|
|
}
|
|
|
|
if (!ent->decel)
|
|
{
|
|
ent->decel = 5;
|
|
}
|
|
else
|
|
{
|
|
ent->decel *= 0.1;
|
|
}
|
|
|
|
if (!ent->dmg)
|
|
{
|
|
ent->dmg = 2;
|
|
}
|
|
|
|
if (!st.lip)
|
|
{
|
|
st.lip = 8;
|
|
}
|
|
|
|
/* pos1 is the top position, pos2 is the bottom */
|
|
VectorCopy(ent->s.origin, ent->pos1);
|
|
VectorCopy(ent->s.origin, ent->pos2);
|
|
|
|
if (st.height)
|
|
{
|
|
ent->pos2[2] -= st.height;
|
|
}
|
|
else
|
|
{
|
|
ent->pos2[2] -= (ent->maxs[2] - ent->mins[2]) - st.lip;
|
|
}
|
|
|
|
ent->use = Use_Plat;
|
|
|
|
plat_spawn_inside_trigger(ent); /* the "start moving" trigger */
|
|
|
|
if (ent->targetname)
|
|
{
|
|
ent->moveinfo.state = STATE_UP;
|
|
}
|
|
else
|
|
{
|
|
VectorCopy(ent->pos2, ent->s.origin);
|
|
gi.linkentity(ent);
|
|
ent->moveinfo.state = STATE_BOTTOM;
|
|
}
|
|
|
|
ent->moveinfo.speed = ent->speed;
|
|
ent->moveinfo.accel = ent->accel;
|
|
ent->moveinfo.decel = ent->decel;
|
|
ent->moveinfo.wait = ent->wait;
|
|
VectorCopy(ent->pos1, ent->moveinfo.start_origin);
|
|
VectorCopy(ent->s.angles, ent->moveinfo.start_angles);
|
|
VectorCopy(ent->pos2, ent->moveinfo.end_origin);
|
|
VectorCopy(ent->s.angles, ent->moveinfo.end_angles);
|
|
|
|
ent->moveinfo.sound_start = gi.soundindex("plats/pt1_strt.wav");
|
|
ent->moveinfo.sound_middle = gi.soundindex("plats/pt1_mid.wav");
|
|
ent->moveinfo.sound_end = gi.soundindex("plats/pt1_end.wav");
|
|
}
|
|
|
|
void
|
|
plat2_spawn_danger_area(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
vec3_t mins, maxs;
|
|
|
|
VectorCopy(ent->mins, mins);
|
|
VectorCopy(ent->maxs, maxs);
|
|
maxs[2] = ent->mins[2] + 64;
|
|
|
|
SpawnBadArea(mins, maxs, 0, ent);
|
|
}
|
|
|
|
void
|
|
plat2_kill_danger_area(edict_t *ent)
|
|
{
|
|
edict_t *t;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
t = NULL;
|
|
|
|
while ((t = G_Find(t, FOFS(classname), "bad_area")))
|
|
{
|
|
if (t->owner == ent)
|
|
{
|
|
G_FreeEdict(t);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
plat2_hit_top(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(ent->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (ent->moveinfo.sound_end)
|
|
{
|
|
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE, ent->moveinfo.sound_end,
|
|
1, ATTN_STATIC, 0);
|
|
}
|
|
|
|
ent->s.sound = 0;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_TOP;
|
|
|
|
if (ent->plat2flags & PLAT2_CALLED)
|
|
{
|
|
ent->plat2flags = PLAT2_WAITING;
|
|
|
|
if (!(ent->spawnflags & PLAT2_TOGGLE))
|
|
{
|
|
ent->think = plat2_go_down;
|
|
ent->nextthink = level.time + 5.0;
|
|
}
|
|
|
|
if (deathmatch->value)
|
|
{
|
|
ent->last_move_time = level.time - 1.0;
|
|
}
|
|
else
|
|
{
|
|
ent->last_move_time = level.time - 2.0;
|
|
}
|
|
}
|
|
else if (!(ent->spawnflags & PLAT2_TOP) &&
|
|
!(ent->spawnflags & PLAT2_TOGGLE))
|
|
{
|
|
ent->plat2flags = 0;
|
|
ent->think = plat2_go_down;
|
|
ent->nextthink = level.time + 2.0;
|
|
ent->last_move_time = level.time;
|
|
}
|
|
else
|
|
{
|
|
ent->plat2flags = 0;
|
|
ent->last_move_time = level.time;
|
|
}
|
|
|
|
G_UseTargets(ent, ent);
|
|
}
|
|
|
|
void
|
|
plat2_hit_bottom(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(ent->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (ent->moveinfo.sound_end)
|
|
{
|
|
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
ent->moveinfo.sound_end, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
ent->s.sound = 0;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_BOTTOM;
|
|
|
|
if (ent->plat2flags & PLAT2_CALLED)
|
|
{
|
|
ent->plat2flags = PLAT2_WAITING;
|
|
|
|
if (!(ent->spawnflags & PLAT2_TOGGLE))
|
|
{
|
|
ent->think = plat2_go_up;
|
|
ent->nextthink = level.time + 5.0;
|
|
}
|
|
|
|
if (deathmatch->value)
|
|
{
|
|
ent->last_move_time = level.time - 1.0;
|
|
}
|
|
else
|
|
{
|
|
ent->last_move_time = level.time - 2.0;
|
|
}
|
|
}
|
|
else if ((ent->spawnflags & PLAT2_TOP) && !(ent->spawnflags & PLAT2_TOGGLE))
|
|
{
|
|
ent->plat2flags = 0;
|
|
ent->think = plat2_go_up;
|
|
ent->nextthink = level.time + 2.0;
|
|
ent->last_move_time = level.time;
|
|
}
|
|
else
|
|
{
|
|
ent->plat2flags = 0;
|
|
ent->last_move_time = level.time;
|
|
}
|
|
|
|
plat2_kill_danger_area(ent);
|
|
G_UseTargets(ent, ent);
|
|
}
|
|
|
|
void
|
|
plat2_go_down(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(ent->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (ent->moveinfo.sound_start)
|
|
{
|
|
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
ent->moveinfo.sound_start, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
ent->s.sound = ent->moveinfo.sound_middle;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_DOWN;
|
|
ent->plat2flags |= PLAT2_MOVING;
|
|
|
|
Move_Calc(ent, ent->moveinfo.end_origin, plat2_hit_bottom);
|
|
}
|
|
|
|
void
|
|
plat2_go_up(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(ent->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (ent->moveinfo.sound_start)
|
|
{
|
|
gi.sound(ent, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
ent->moveinfo.sound_start, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
ent->s.sound = ent->moveinfo.sound_middle;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_UP;
|
|
ent->plat2flags |= PLAT2_MOVING;
|
|
|
|
plat2_spawn_danger_area(ent);
|
|
|
|
Move_Calc(ent, ent->moveinfo.start_origin, plat2_hit_top);
|
|
}
|
|
|
|
void
|
|
plat2_operate(edict_t *ent, edict_t *other)
|
|
{
|
|
int otherState;
|
|
float pauseTime;
|
|
float platCenter;
|
|
edict_t *trigger;
|
|
|
|
if (!ent || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
trigger = ent;
|
|
ent = ent->enemy; /* now point at the plat, not the trigger */
|
|
|
|
if (ent->plat2flags & PLAT2_MOVING)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((ent->last_move_time + 2) > level.time)
|
|
{
|
|
return;
|
|
}
|
|
|
|
platCenter = (trigger->absmin[2] + trigger->absmax[2]) / 2;
|
|
|
|
if (ent->moveinfo.state == STATE_TOP)
|
|
{
|
|
otherState = STATE_TOP;
|
|
|
|
if (ent->spawnflags & PLAT2_BOX_LIFT)
|
|
{
|
|
if (platCenter > other->s.origin[2])
|
|
{
|
|
otherState = STATE_BOTTOM;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (trigger->absmax[2] > other->s.origin[2])
|
|
{
|
|
otherState = STATE_BOTTOM;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
otherState = STATE_BOTTOM;
|
|
|
|
if (other->s.origin[2] > platCenter)
|
|
{
|
|
otherState = STATE_TOP;
|
|
}
|
|
}
|
|
|
|
ent->plat2flags = PLAT2_MOVING;
|
|
|
|
if (deathmatch->value)
|
|
{
|
|
pauseTime = 0.3;
|
|
}
|
|
else
|
|
{
|
|
pauseTime = 0.5;
|
|
}
|
|
|
|
if (ent->moveinfo.state != otherState)
|
|
{
|
|
ent->plat2flags |= PLAT2_CALLED;
|
|
pauseTime = 0.1;
|
|
}
|
|
|
|
ent->last_move_time = level.time;
|
|
|
|
if (ent->moveinfo.state == STATE_BOTTOM)
|
|
{
|
|
ent->think = plat2_go_up;
|
|
ent->nextthink = level.time + pauseTime;
|
|
}
|
|
else
|
|
{
|
|
ent->think = plat2_go_down;
|
|
ent->nextthink = level.time + pauseTime;
|
|
}
|
|
}
|
|
|
|
void
|
|
Touch_Plat_Center2(edict_t *ent, edict_t *other,
|
|
cplane_t *plane /* unused */, csurface_t *surf /* unused */)
|
|
{
|
|
if (!ent || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* this requires monsters to actively trigger plats, not just step on them. */
|
|
if (other->health <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* don't let non-monsters activate plat2s */
|
|
if ((!(other->svflags & SVF_MONSTER)) && (!other->client))
|
|
{
|
|
return;
|
|
}
|
|
|
|
plat2_operate(ent, other);
|
|
}
|
|
|
|
void
|
|
plat2_blocked(edict_t *self, edict_t *other)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(other->svflags & SVF_MONSTER) && (!other->client))
|
|
{
|
|
/* give it a chance to go away on it's own terms (like gibs) */
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, 100000, 1, 0, MOD_CRUSH);
|
|
|
|
/* if it's still there, nuke it */
|
|
if (other->inuse)
|
|
{
|
|
BecomeExplosion1(other);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/* gib dead things */
|
|
if (other->health < 1)
|
|
{
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, 100, 1, 0, MOD_CRUSH);
|
|
}
|
|
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
|
|
|
|
if (self->moveinfo.state == STATE_UP)
|
|
{
|
|
plat2_go_down(self);
|
|
}
|
|
else if (self->moveinfo.state == STATE_DOWN)
|
|
{
|
|
plat2_go_up(self);
|
|
}
|
|
}
|
|
|
|
void
|
|
Use_Plat2(edict_t *ent, edict_t *other /* unused */,
|
|
edict_t *activator)
|
|
{
|
|
edict_t *trigger;
|
|
int i;
|
|
|
|
if (!ent || !activator)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ent->moveinfo.state > STATE_BOTTOM)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((ent->last_move_time + 2) > level.time)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (i = 1, trigger = g_edicts + 1; i < globals.num_edicts; i++, trigger++)
|
|
{
|
|
if (!trigger->inuse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (trigger->touch == Touch_Plat_Center2)
|
|
{
|
|
if (trigger->enemy == ent)
|
|
{
|
|
plat2_operate(trigger, activator);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
plat2_activate(edict_t *ent, edict_t *other /* unused */,
|
|
edict_t *activator /* unused */)
|
|
{
|
|
edict_t *trigger;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ent->use = Use_Plat2;
|
|
trigger = plat_spawn_inside_trigger(ent); /* the "start moving" trigger */
|
|
|
|
trigger->maxs[0] += 10;
|
|
trigger->maxs[1] += 10;
|
|
trigger->mins[0] -= 10;
|
|
trigger->mins[1] -= 10;
|
|
|
|
gi.linkentity(trigger);
|
|
|
|
trigger->touch = Touch_Plat_Center2; /* Override trigger touch function */
|
|
|
|
plat2_go_down(ent);
|
|
}
|
|
|
|
/* QUAKED func_plat2 (0 .5 .8) ? PLAT_LOW_TRIGGER PLAT2_TOGGLE PLAT2_TOP PLAT2_TRIGGER_TOP PLAT2_TRIGGER_BOTTOM BOX_LIFT
|
|
* speed default 150
|
|
*
|
|
* PLAT_LOW_TRIGGER - creates a short trigger field at the bottom
|
|
* PLAT2_TOGGLE - plat will not return to default position.
|
|
* PLAT2_TOP - plat's default position will the the top.
|
|
* PLAT2_TRIGGER_TOP - plat will trigger it's targets each time it hits top
|
|
* PLAT2_TRIGGER_BOTTOM - plat will trigger it's targets each time it hits bottom
|
|
* BOX_LIFT - this indicates that the lift is a box, rather than just a platform
|
|
*
|
|
* Plats are always drawn in the extended position, so they will light correctly.
|
|
*
|
|
* If the plat is the target of another trigger or button, it will start out
|
|
* disabled in the extended position until it is trigger, when it will lower
|
|
* and become a normal plat.
|
|
*
|
|
* "speed" overrides default 200.
|
|
* "accel" overrides default 500
|
|
* "lip" no default
|
|
*
|
|
* If the "height" key is set, that will determine the amount the plat moves,
|
|
* instead of being implicitly determoveinfoned by the model's height.
|
|
*
|
|
*/
|
|
void
|
|
SP_func_plat2(edict_t *ent)
|
|
{
|
|
edict_t *trigger;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorClear(ent->s.angles);
|
|
ent->solid = SOLID_BSP;
|
|
ent->movetype = MOVETYPE_PUSH;
|
|
|
|
gi.setmodel(ent, ent->model);
|
|
|
|
ent->blocked = plat2_blocked;
|
|
|
|
if (!ent->speed)
|
|
{
|
|
ent->speed = 20;
|
|
}
|
|
else
|
|
{
|
|
ent->speed *= 0.1;
|
|
}
|
|
|
|
if (!ent->accel)
|
|
{
|
|
ent->accel = 5;
|
|
}
|
|
else
|
|
{
|
|
ent->accel *= 0.1;
|
|
}
|
|
|
|
if (!ent->decel)
|
|
{
|
|
ent->decel = 5;
|
|
}
|
|
else
|
|
{
|
|
ent->decel *= 0.1;
|
|
}
|
|
|
|
if (deathmatch->value)
|
|
{
|
|
ent->speed *= 2;
|
|
ent->accel *= 2;
|
|
ent->decel *= 2;
|
|
}
|
|
|
|
/* Added to kill things it's being blocked by */
|
|
if (!ent->dmg)
|
|
{
|
|
ent->dmg = 2;
|
|
}
|
|
|
|
/* pos1 is the top position, pos2 is the bottom */
|
|
VectorCopy(ent->s.origin, ent->pos1);
|
|
VectorCopy(ent->s.origin, ent->pos2);
|
|
|
|
if (st.height)
|
|
{
|
|
ent->pos2[2] -= (st.height - st.lip);
|
|
}
|
|
else
|
|
{
|
|
ent->pos2[2] -= (ent->maxs[2] - ent->mins[2]) - st.lip;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_TOP;
|
|
|
|
if (ent->targetname)
|
|
{
|
|
ent->use = plat2_activate;
|
|
}
|
|
else
|
|
{
|
|
ent->use = Use_Plat2;
|
|
|
|
trigger = plat_spawn_inside_trigger(ent); /* the "start moving" trigger */
|
|
|
|
trigger->maxs[0] += 10;
|
|
trigger->maxs[1] += 10;
|
|
trigger->mins[0] -= 10;
|
|
trigger->mins[1] -= 10;
|
|
|
|
gi.linkentity(trigger);
|
|
trigger->touch = Touch_Plat_Center2; /* Override trigger touch function */
|
|
|
|
if (!(ent->spawnflags & PLAT2_TOP))
|
|
{
|
|
VectorCopy(ent->pos2, ent->s.origin);
|
|
ent->moveinfo.state = STATE_BOTTOM;
|
|
}
|
|
}
|
|
|
|
gi.linkentity(ent);
|
|
|
|
ent->moveinfo.speed = ent->speed;
|
|
ent->moveinfo.accel = ent->accel;
|
|
ent->moveinfo.decel = ent->decel;
|
|
ent->moveinfo.wait = ent->wait;
|
|
VectorCopy(ent->pos1, ent->moveinfo.start_origin);
|
|
VectorCopy(ent->s.angles, ent->moveinfo.start_angles);
|
|
VectorCopy(ent->pos2, ent->moveinfo.end_origin);
|
|
VectorCopy(ent->s.angles, ent->moveinfo.end_angles);
|
|
|
|
ent->moveinfo.sound_start = gi.soundindex("plats/pt1_strt.wav");
|
|
ent->moveinfo.sound_middle = gi.soundindex("plats/pt1_mid.wav");
|
|
ent->moveinfo.sound_end = gi.soundindex("plats/pt1_end.wav");
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/* QUAKED func_rotating (0 .5 .8) ? START_ON REVERSE X_AXIS Y_AXIS TOUCH_PAIN STOP ANIMATED ANIMATED_FAST EAST MED HARD DM COOP ACCEL
|
|
*
|
|
* You need to have an origin brush as part of this entity. The center
|
|
* of that brush will bethe point around which it is rotated. It will
|
|
* rotate around the Z axis by default. You can check either the
|
|
* X_AXIS or Y_AXIS box to change that.
|
|
*
|
|
* func_rotating will use it's targets when it stops and starts.
|
|
*
|
|
* "speed" determines how fast it moves; default value is 100.
|
|
* "dmg" damage to inflict when blocked (2 default)
|
|
* "accel" if specified, is how much the rotation speed will increase per .1sec.
|
|
*
|
|
* REVERSE will cause the it to rotate in the opposite direction.
|
|
* STOP mean it will stop moving instead of pushing entities
|
|
* ACCEL means it will accelerate to it's final speed and decelerate when shutting down.
|
|
*/
|
|
void
|
|
rotating_accel(edict_t *self)
|
|
{
|
|
float current_speed;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
current_speed = VectorLength(self->avelocity);
|
|
|
|
if (current_speed >= (self->speed - self->accel)) /* done */
|
|
{
|
|
VectorScale(self->movedir, self->speed, self->avelocity);
|
|
G_UseTargets(self, self);
|
|
}
|
|
else
|
|
{
|
|
current_speed += self->accel;
|
|
VectorScale(self->movedir, current_speed, self->avelocity);
|
|
self->think = rotating_accel;
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
}
|
|
|
|
void
|
|
rotating_decel(edict_t *self)
|
|
{
|
|
float current_speed;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
current_speed = VectorLength(self->avelocity);
|
|
|
|
if (current_speed <= self->decel) /* done */
|
|
{
|
|
VectorClear(self->avelocity);
|
|
G_UseTargets(self, self);
|
|
self->touch = NULL;
|
|
}
|
|
else
|
|
{
|
|
current_speed -= self->decel;
|
|
VectorScale(self->movedir, current_speed, self->avelocity);
|
|
self->think = rotating_decel;
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
}
|
|
|
|
void
|
|
rotating_blocked(edict_t *self, edict_t *other)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
|
|
}
|
|
|
|
void
|
|
rotating_touch(edict_t *self, edict_t *other, cplane_t *plane /* unused */,
|
|
csurface_t *surf /* unused */)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->avelocity[0] || self->avelocity[1] || self->avelocity[2])
|
|
{
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
|
|
}
|
|
}
|
|
|
|
void
|
|
rotating_use(edict_t *self, edict_t *other /* unused */,
|
|
edict_t *activator /* unused */)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!VectorCompare(self->avelocity, vec3_origin))
|
|
{
|
|
self->s.sound = 0;
|
|
|
|
if (self->spawnflags & 8192) /* Decelerate */
|
|
{
|
|
rotating_decel(self);
|
|
}
|
|
else
|
|
{
|
|
VectorClear(self->avelocity);
|
|
G_UseTargets(self, self);
|
|
self->touch = NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->s.sound = self->moveinfo.sound_middle;
|
|
|
|
if (self->spawnflags & 8192) /* accelerate */
|
|
{
|
|
rotating_accel(self);
|
|
}
|
|
else
|
|
{
|
|
VectorScale(self->movedir, self->speed, self->avelocity);
|
|
G_UseTargets(self, self);
|
|
}
|
|
|
|
if (self->spawnflags & 16)
|
|
{
|
|
self->touch = rotating_touch;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
SP_func_rotating(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ent->solid = SOLID_BSP;
|
|
|
|
if (ent->spawnflags & 32)
|
|
{
|
|
ent->movetype = MOVETYPE_STOP;
|
|
}
|
|
else
|
|
{
|
|
ent->movetype = MOVETYPE_PUSH;
|
|
}
|
|
|
|
/* set the axis of rotation */
|
|
VectorClear(ent->movedir);
|
|
|
|
if (ent->spawnflags & 4)
|
|
{
|
|
ent->movedir[2] = 1.0;
|
|
}
|
|
else if (ent->spawnflags & 8)
|
|
{
|
|
ent->movedir[0] = 1.0;
|
|
}
|
|
else /* Z_AXIS */
|
|
{
|
|
ent->movedir[1] = 1.0;
|
|
}
|
|
|
|
/* check for reverse rotation */
|
|
if (ent->spawnflags & 2)
|
|
{
|
|
VectorNegate(ent->movedir, ent->movedir);
|
|
}
|
|
|
|
if (!ent->speed)
|
|
{
|
|
ent->speed = 100;
|
|
}
|
|
|
|
if (!ent->dmg)
|
|
{
|
|
ent->dmg = 2;
|
|
}
|
|
|
|
ent->use = rotating_use;
|
|
|
|
ent->blocked = rotating_blocked;
|
|
|
|
if (ent->spawnflags & 1)
|
|
{
|
|
ent->use(ent, NULL, NULL);
|
|
}
|
|
|
|
if (ent->spawnflags & 64)
|
|
{
|
|
ent->s.effects |= EF_ANIM_ALL;
|
|
}
|
|
|
|
if (ent->spawnflags & 128)
|
|
{
|
|
ent->s.effects |= EF_ANIM_ALLFAST;
|
|
}
|
|
|
|
if (ent->spawnflags & 8192) /* Accelerate / Decelerate */
|
|
{
|
|
if (!ent->accel)
|
|
{
|
|
ent->accel = 1;
|
|
}
|
|
else if (ent->accel > ent->speed)
|
|
{
|
|
ent->accel = ent->speed;
|
|
}
|
|
|
|
if (!ent->decel)
|
|
{
|
|
ent->decel = 1;
|
|
}
|
|
else if (ent->decel > ent->speed)
|
|
{
|
|
ent->decel = ent->speed;
|
|
}
|
|
}
|
|
|
|
gi.setmodel(ent, ent->model);
|
|
gi.linkentity(ent);
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/* BUTTONS */
|
|
|
|
/*
|
|
* QUAKED func_button (0 .5 .8) ?
|
|
*
|
|
* When a button is touched, it moves some distance
|
|
* in the direction of it's angle, triggers all of it's
|
|
* targets, waits some time, then returns to it's original
|
|
* position where it can be triggered again.
|
|
*
|
|
* "angle" determines the opening direction
|
|
* "target" all entities with a matching targetname will be used
|
|
* "speed" override the default 40 speed
|
|
* "wait" override the default 1 second wait (-1 = never return)
|
|
* "lip" override the default 4 pixel lip remaining at end of move
|
|
* "health" if set, the button must be killed instead of touched
|
|
* "sounds"
|
|
* 1) silent
|
|
* 2) steam metal
|
|
* 3) wooden clunk
|
|
* 4) metallic click
|
|
* 5) in-out
|
|
*/
|
|
void
|
|
button_done(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->moveinfo.state = STATE_BOTTOM;
|
|
self->s.effects &= ~EF_ANIM23;
|
|
self->s.effects |= EF_ANIM01;
|
|
}
|
|
|
|
void
|
|
button_return(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->moveinfo.state = STATE_DOWN;
|
|
|
|
Move_Calc(self, self->moveinfo.start_origin, button_done);
|
|
|
|
self->s.frame = 0;
|
|
|
|
if (self->health)
|
|
{
|
|
self->takedamage = DAMAGE_YES;
|
|
}
|
|
}
|
|
|
|
void
|
|
button_wait(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->moveinfo.state = STATE_TOP;
|
|
self->s.effects &= ~EF_ANIM01;
|
|
self->s.effects |= EF_ANIM23;
|
|
|
|
G_UseTargets(self, self->activator);
|
|
self->s.frame = 1;
|
|
|
|
if (self->moveinfo.wait >= 0)
|
|
{
|
|
self->nextthink = level.time + self->moveinfo.wait;
|
|
self->think = button_return;
|
|
}
|
|
}
|
|
|
|
void
|
|
button_fire(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((self->moveinfo.state == STATE_UP) ||
|
|
(self->moveinfo.state == STATE_TOP))
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->moveinfo.state = STATE_UP;
|
|
|
|
if (self->moveinfo.sound_start && !(self->flags & FL_TEAMSLAVE))
|
|
{
|
|
gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
self->moveinfo.sound_start,
|
|
1, ATTN_STATIC, 0);
|
|
}
|
|
|
|
Move_Calc(self, self->moveinfo.end_origin, button_wait);
|
|
}
|
|
|
|
void
|
|
button_use(edict_t *self, edict_t *other /* unused */, edict_t *activator)
|
|
{
|
|
if (!self || !activator)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->activator = activator;
|
|
button_fire(self);
|
|
}
|
|
|
|
void
|
|
button_touch(edict_t *self, edict_t *other, cplane_t *plane /* unused */,
|
|
csurface_t *surf /* unused */)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!other->client)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (other->health <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->activator = other;
|
|
button_fire(self);
|
|
}
|
|
|
|
void
|
|
button_killed(edict_t *self, edict_t *inflictor /* unused */,
|
|
edict_t *attacker, int damage /* unused */,
|
|
vec3_t point /* unused */)
|
|
{
|
|
if (!self || !attacker)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->activator = attacker;
|
|
self->health = self->max_health;
|
|
self->takedamage = DAMAGE_NO;
|
|
button_fire(self);
|
|
}
|
|
|
|
void
|
|
SP_func_button(edict_t *ent)
|
|
{
|
|
vec3_t abs_movedir;
|
|
float dist;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
G_SetMovedir(ent->s.angles, ent->movedir);
|
|
ent->movetype = MOVETYPE_STOP;
|
|
ent->solid = SOLID_BSP;
|
|
gi.setmodel(ent, ent->model);
|
|
|
|
if (ent->sounds != 1)
|
|
{
|
|
ent->moveinfo.sound_start = gi.soundindex("switches/butn2.wav");
|
|
}
|
|
|
|
if (!ent->speed)
|
|
{
|
|
ent->speed = 40;
|
|
}
|
|
|
|
if (!ent->accel)
|
|
{
|
|
ent->accel = ent->speed;
|
|
}
|
|
|
|
if (!ent->decel)
|
|
{
|
|
ent->decel = ent->speed;
|
|
}
|
|
|
|
if (!ent->wait)
|
|
{
|
|
ent->wait = 3;
|
|
}
|
|
|
|
if (!st.lip)
|
|
{
|
|
st.lip = 4;
|
|
}
|
|
|
|
VectorCopy(ent->s.origin, ent->pos1);
|
|
abs_movedir[0] = fabs(ent->movedir[0]);
|
|
abs_movedir[1] = fabs(ent->movedir[1]);
|
|
abs_movedir[2] = fabs(ent->movedir[2]);
|
|
dist = abs_movedir[0] * ent->size[0] + abs_movedir[1] * ent->size[1] +
|
|
abs_movedir[2] * ent->size[2] - st.lip;
|
|
VectorMA(ent->pos1, dist, ent->movedir, ent->pos2);
|
|
|
|
ent->use = button_use;
|
|
ent->s.effects |= EF_ANIM01;
|
|
|
|
if (ent->health)
|
|
{
|
|
ent->max_health = ent->health;
|
|
ent->die = button_killed;
|
|
ent->takedamage = DAMAGE_YES;
|
|
}
|
|
else if (!ent->targetname)
|
|
{
|
|
ent->touch = button_touch;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_BOTTOM;
|
|
|
|
ent->moveinfo.speed = ent->speed;
|
|
ent->moveinfo.accel = ent->accel;
|
|
ent->moveinfo.decel = ent->decel;
|
|
ent->moveinfo.wait = ent->wait;
|
|
VectorCopy(ent->pos1, ent->moveinfo.start_origin);
|
|
VectorCopy(ent->s.angles, ent->moveinfo.start_angles);
|
|
VectorCopy(ent->pos2, ent->moveinfo.end_origin);
|
|
VectorCopy(ent->s.angles, ent->moveinfo.end_angles);
|
|
|
|
gi.linkentity(ent);
|
|
}
|
|
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* DOORS
|
|
*
|
|
* spawn a trigger surrounding the entire team
|
|
* unless it is already targeted by another
|
|
*/
|
|
|
|
/*
|
|
* QUAKED func_door (0 .5 .8) ? START_OPEN x CRUSHER NOMONSTER ANIMATED TOGGLE ANIMATED_FAST
|
|
*
|
|
* TOGGLE wait in both the start and end states for a trigger event.
|
|
* START_OPEN the door to moves to its destination when spawned, and operate in reverse.
|
|
* It is used to temporarily or permanently close off an area when triggered
|
|
* (not useful for touch or takedamage doors).
|
|
* NOMONSTER monsters will not trigger this door
|
|
*
|
|
* "message" is printed when the door is touched if it is a trigger door and it hasn't been fired yet
|
|
* "angle" determines the opening direction
|
|
* "targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door.
|
|
* "health" if set, door must be shot open
|
|
* "speed" movement speed (100 default)
|
|
* "wait" wait before returning (3 default, -1 = never return)
|
|
* "lip" lip remaining at end of move (8 default)
|
|
* "dmg" damage to inflict when blocked (2 default)
|
|
* "sounds"
|
|
* 1) silent
|
|
* 2) light
|
|
* 3) medium
|
|
* 4) heavy
|
|
*/
|
|
|
|
void
|
|
door_use_areaportals(edict_t *self, qboolean open)
|
|
{
|
|
edict_t *t = NULL;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!self->target)
|
|
{
|
|
return;
|
|
}
|
|
|
|
while ((t = G_Find(t, FOFS(targetname), self->target)))
|
|
{
|
|
if (Q_stricmp(t->classname, "func_areaportal") == 0)
|
|
{
|
|
gi.SetAreaPortalState(t->style, open);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
door_hit_top(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(self->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (self->moveinfo.sound_end)
|
|
{
|
|
gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
self->moveinfo.sound_end, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
self->s.sound = 0;
|
|
}
|
|
|
|
self->moveinfo.state = STATE_TOP;
|
|
|
|
if (self->spawnflags & DOOR_TOGGLE)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->moveinfo.wait >= 0)
|
|
{
|
|
self->think = door_go_down;
|
|
self->nextthink = level.time + self->moveinfo.wait;
|
|
}
|
|
}
|
|
|
|
void
|
|
door_hit_bottom(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(self->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (self->moveinfo.sound_end)
|
|
{
|
|
gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
self->moveinfo.sound_end, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
self->s.sound = 0;
|
|
}
|
|
|
|
self->moveinfo.state = STATE_BOTTOM;
|
|
door_use_areaportals(self, false);
|
|
}
|
|
|
|
void
|
|
door_go_down(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(self->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (self->moveinfo.sound_start)
|
|
{
|
|
gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
self->moveinfo.sound_start,
|
|
1, ATTN_STATIC, 0);
|
|
}
|
|
|
|
self->s.sound = self->moveinfo.sound_middle;
|
|
}
|
|
|
|
if (self->max_health)
|
|
{
|
|
self->takedamage = DAMAGE_YES;
|
|
self->health = self->max_health;
|
|
}
|
|
|
|
self->moveinfo.state = STATE_DOWN;
|
|
|
|
if (strcmp(self->classname, "func_door") == 0)
|
|
{
|
|
Move_Calc(self, self->moveinfo.start_origin, door_hit_bottom);
|
|
}
|
|
else if (strcmp(self->classname, "func_door_rotating") == 0)
|
|
{
|
|
AngleMove_Calc(self, door_hit_bottom);
|
|
}
|
|
}
|
|
|
|
void
|
|
door_go_up(edict_t *self, edict_t *activator)
|
|
{
|
|
if (!self || !activator)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->moveinfo.state == STATE_UP)
|
|
{
|
|
return; /* already going up */
|
|
}
|
|
|
|
if (self->moveinfo.state == STATE_TOP)
|
|
{
|
|
/* reset top wait time */
|
|
if (self->moveinfo.wait >= 0)
|
|
{
|
|
self->nextthink = level.time + self->moveinfo.wait;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!(self->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (self->moveinfo.sound_start)
|
|
{
|
|
gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
self->moveinfo.sound_start, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
self->s.sound = self->moveinfo.sound_middle;
|
|
}
|
|
|
|
self->moveinfo.state = STATE_UP;
|
|
|
|
if (strcmp(self->classname, "func_door") == 0)
|
|
{
|
|
Move_Calc(self, self->moveinfo.end_origin, door_hit_top);
|
|
}
|
|
else if (strcmp(self->classname, "func_door_rotating") == 0)
|
|
{
|
|
AngleMove_Calc(self, door_hit_top);
|
|
}
|
|
|
|
G_UseTargets(self, activator);
|
|
door_use_areaportals(self, true);
|
|
}
|
|
|
|
void
|
|
smart_water_go_up(edict_t *self)
|
|
{
|
|
float distance;
|
|
edict_t *lowestPlayer;
|
|
edict_t *ent;
|
|
float lowestPlayerPt;
|
|
int i;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->moveinfo.state == STATE_TOP)
|
|
{
|
|
/* reset top wait time */
|
|
if (self->moveinfo.wait >= 0)
|
|
{
|
|
self->nextthink = level.time + self->moveinfo.wait;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (self->health)
|
|
{
|
|
if (self->absmax[2] >= self->health)
|
|
{
|
|
VectorClear(self->velocity);
|
|
self->nextthink = 0;
|
|
self->moveinfo.state = STATE_TOP;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!(self->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (self->moveinfo.sound_start)
|
|
{
|
|
gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
self->moveinfo.sound_start, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
self->s.sound = self->moveinfo.sound_middle;
|
|
}
|
|
|
|
/* find the lowest player point. */
|
|
lowestPlayerPt = 999999;
|
|
lowestPlayer = NULL;
|
|
|
|
for (i = 0; i < game.maxclients; i++)
|
|
{
|
|
ent = &g_edicts[1 + i];
|
|
|
|
/* don't count dead or unused player slots */
|
|
if ((ent->inuse) && (ent->health > 0))
|
|
{
|
|
if (ent->absmin[2] < lowestPlayerPt)
|
|
{
|
|
lowestPlayerPt = ent->absmin[2];
|
|
lowestPlayer = ent;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!lowestPlayer)
|
|
{
|
|
return;
|
|
}
|
|
|
|
distance = lowestPlayerPt - self->absmax[2];
|
|
|
|
/* for the calculations, make sure we
|
|
intend to go up at least a little. */
|
|
if (distance < self->accel)
|
|
{
|
|
distance = 100;
|
|
self->moveinfo.speed = 5;
|
|
}
|
|
else
|
|
{
|
|
self->moveinfo.speed = distance / self->accel;
|
|
}
|
|
|
|
if (self->moveinfo.speed < 5)
|
|
{
|
|
self->moveinfo.speed = 5;
|
|
}
|
|
else if (self->moveinfo.speed > self->speed)
|
|
{
|
|
self->moveinfo.speed = self->speed;
|
|
}
|
|
|
|
/* should this allow any movement other than straight up? */
|
|
VectorSet(self->moveinfo.dir, 0, 0, 1);
|
|
VectorScale(self->moveinfo.dir, self->moveinfo.speed, self->velocity);
|
|
self->moveinfo.remaining_distance = distance;
|
|
|
|
if (self->moveinfo.state != STATE_UP)
|
|
{
|
|
G_UseTargets(self, lowestPlayer);
|
|
door_use_areaportals(self, true);
|
|
self->moveinfo.state = STATE_UP;
|
|
}
|
|
|
|
self->think = smart_water_go_up;
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
void
|
|
door_use(edict_t *self, edict_t *other /* unused */, edict_t *activator)
|
|
{
|
|
edict_t *ent;
|
|
vec3_t center;
|
|
|
|
if (!self || !activator)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->flags & FL_TEAMSLAVE)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->spawnflags & DOOR_TOGGLE)
|
|
{
|
|
if ((self->moveinfo.state == STATE_UP) ||
|
|
(self->moveinfo.state == STATE_TOP))
|
|
{
|
|
/* trigger all paired doors */
|
|
for (ent = self; ent; ent = ent->teamchain)
|
|
{
|
|
ent->message = NULL;
|
|
ent->touch = NULL;
|
|
door_go_down(ent);
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* smart water is different */
|
|
VectorAdd(self->mins, self->maxs, center);
|
|
VectorScale(center, 0.5, center);
|
|
|
|
if ((gi.pointcontents(center) & MASK_WATER) && self->spawnflags & 2)
|
|
{
|
|
self->message = NULL;
|
|
self->touch = NULL;
|
|
self->enemy = activator;
|
|
smart_water_go_up(self);
|
|
return;
|
|
}
|
|
|
|
/* trigger all paired doors */
|
|
for (ent = self; ent; ent = ent->teamchain)
|
|
{
|
|
ent->message = NULL;
|
|
ent->touch = NULL;
|
|
door_go_up(ent, activator);
|
|
}
|
|
}
|
|
|
|
void
|
|
Touch_DoorTrigger(edict_t *self, edict_t *other, cplane_t *plane /* unused */,
|
|
csurface_t *surf /* unused */)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (other->health <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(other->svflags & SVF_MONSTER) && (!other->client))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ((self->owner->spawnflags & DOOR_NOMONSTER) &&
|
|
(other->svflags & SVF_MONSTER))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (level.time < self->touch_debounce_time)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->touch_debounce_time = level.time + 1.0;
|
|
|
|
door_use(self->owner, other, other);
|
|
}
|
|
|
|
void
|
|
Think_CalcMoveSpeed(edict_t *self)
|
|
{
|
|
edict_t *ent;
|
|
float min;
|
|
float time;
|
|
float newspeed;
|
|
float ratio;
|
|
float dist;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->flags & FL_TEAMSLAVE)
|
|
{
|
|
return; /* only the team master does this */
|
|
}
|
|
|
|
/* find the smallest distance any member of the team will be moving */
|
|
min = fabs(self->moveinfo.distance);
|
|
|
|
for (ent = self->teamchain; ent; ent = ent->teamchain)
|
|
{
|
|
dist = fabs(ent->moveinfo.distance);
|
|
|
|
if (dist < min)
|
|
{
|
|
min = dist;
|
|
}
|
|
}
|
|
|
|
time = min / self->moveinfo.speed;
|
|
|
|
/* adjust speeds so they will all complete at the same time */
|
|
for (ent = self; ent; ent = ent->teamchain)
|
|
{
|
|
newspeed = fabs(ent->moveinfo.distance) / time;
|
|
ratio = newspeed / ent->moveinfo.speed;
|
|
|
|
if (ent->moveinfo.accel == ent->moveinfo.speed)
|
|
{
|
|
ent->moveinfo.accel = newspeed;
|
|
}
|
|
else
|
|
{
|
|
ent->moveinfo.accel *= ratio;
|
|
}
|
|
|
|
if (ent->moveinfo.decel == ent->moveinfo.speed)
|
|
{
|
|
ent->moveinfo.decel = newspeed;
|
|
}
|
|
else
|
|
{
|
|
ent->moveinfo.decel *= ratio;
|
|
}
|
|
|
|
ent->moveinfo.speed = newspeed;
|
|
}
|
|
}
|
|
|
|
void
|
|
Think_SpawnDoorTrigger(edict_t *ent)
|
|
{
|
|
edict_t *other;
|
|
vec3_t mins, maxs;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ent->flags & FL_TEAMSLAVE)
|
|
{
|
|
return; /* only the team leader spawns a trigger */
|
|
}
|
|
|
|
VectorCopy(ent->absmin, mins);
|
|
VectorCopy(ent->absmax, maxs);
|
|
|
|
for (other = ent->teamchain; other; other = other->teamchain)
|
|
{
|
|
AddPointToBounds(other->absmin, mins, maxs);
|
|
AddPointToBounds(other->absmax, mins, maxs);
|
|
}
|
|
|
|
/* expand */
|
|
mins[0] -= 60;
|
|
mins[1] -= 60;
|
|
maxs[0] += 60;
|
|
maxs[1] += 60;
|
|
|
|
other = G_Spawn();
|
|
VectorCopy(mins, other->mins);
|
|
VectorCopy(maxs, other->maxs);
|
|
other->owner = ent;
|
|
other->solid = SOLID_TRIGGER;
|
|
other->movetype = MOVETYPE_NONE;
|
|
other->touch = Touch_DoorTrigger;
|
|
gi.linkentity(other);
|
|
|
|
if (ent->spawnflags & DOOR_START_OPEN)
|
|
{
|
|
door_use_areaportals(ent, true);
|
|
}
|
|
|
|
Think_CalcMoveSpeed(ent);
|
|
}
|
|
|
|
void
|
|
door_blocked(edict_t *self, edict_t *other)
|
|
{
|
|
edict_t *ent;
|
|
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(other->svflags & SVF_MONSTER) && (!other->client))
|
|
{
|
|
/* give it a chance to go away on it's own terms (like gibs) */
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, 100000, 1, 0, MOD_CRUSH);
|
|
|
|
/* if it's still there, nuke it */
|
|
if (other->inuse)
|
|
{
|
|
/* Hack for entitiy without their origin near the model */
|
|
VectorMA(other->absmin, 0.5, other->size, other->s.origin);
|
|
BecomeExplosion1(other);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
|
|
|
|
if (self->spawnflags & DOOR_CRUSHER)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* if a door has a negative wait, it would never come
|
|
back if blocked, so let it just squash the object
|
|
to death real fast */
|
|
if (self->moveinfo.wait >= 0)
|
|
{
|
|
if (self->moveinfo.state == STATE_DOWN)
|
|
{
|
|
for (ent = self->teammaster; ent; ent = ent->teamchain)
|
|
{
|
|
door_go_up(ent, ent->activator);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (ent = self->teammaster; ent; ent = ent->teamchain)
|
|
{
|
|
door_go_down(ent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
door_killed(edict_t *self, edict_t *inflictor /* unused */,
|
|
edict_t *attacker, int damage /* unused */,
|
|
vec3_t point /* unused */)
|
|
{
|
|
edict_t *ent;
|
|
|
|
if (!self || !attacker)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (ent = self->teammaster; ent; ent = ent->teamchain)
|
|
{
|
|
ent->health = ent->max_health;
|
|
ent->takedamage = DAMAGE_NO;
|
|
}
|
|
|
|
door_use(self->teammaster, attacker, attacker);
|
|
}
|
|
|
|
void
|
|
door_touch(edict_t *self, edict_t *other, cplane_t *plane /* unused */,
|
|
csurface_t *surf /* unused */)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!other->client)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (level.time < self->touch_debounce_time)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->touch_debounce_time = level.time + 5.0;
|
|
|
|
gi.centerprintf(other, "%s", self->message);
|
|
gi.sound(other, CHAN_AUTO, gi.soundindex("misc/talk1.wav"), 1, ATTN_NORM, 0);
|
|
}
|
|
|
|
void
|
|
SP_func_door(edict_t *ent)
|
|
{
|
|
vec3_t abs_movedir;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ent->sounds != 1)
|
|
{
|
|
ent->moveinfo.sound_start = gi.soundindex("doors/dr1_strt.wav");
|
|
ent->moveinfo.sound_middle = gi.soundindex("doors/dr1_mid.wav");
|
|
ent->moveinfo.sound_end = gi.soundindex("doors/dr1_end.wav");
|
|
}
|
|
|
|
G_SetMovedir(ent->s.angles, ent->movedir);
|
|
ent->movetype = MOVETYPE_PUSH;
|
|
ent->solid = SOLID_BSP;
|
|
gi.setmodel(ent, ent->model);
|
|
|
|
ent->blocked = door_blocked;
|
|
ent->use = door_use;
|
|
|
|
if (!ent->speed)
|
|
{
|
|
ent->speed = 100;
|
|
}
|
|
|
|
if (deathmatch->value)
|
|
{
|
|
ent->speed *= 2;
|
|
}
|
|
|
|
if (!ent->accel)
|
|
{
|
|
ent->accel = ent->speed;
|
|
}
|
|
|
|
if (!ent->decel)
|
|
{
|
|
ent->decel = ent->speed;
|
|
}
|
|
|
|
if (!ent->wait)
|
|
{
|
|
ent->wait = 3;
|
|
}
|
|
|
|
if (!st.lip)
|
|
{
|
|
st.lip = 8;
|
|
}
|
|
|
|
if (!ent->dmg)
|
|
{
|
|
ent->dmg = 2;
|
|
}
|
|
|
|
/* calculate second position */
|
|
VectorCopy(ent->s.origin, ent->pos1);
|
|
abs_movedir[0] = fabs(ent->movedir[0]);
|
|
abs_movedir[1] = fabs(ent->movedir[1]);
|
|
abs_movedir[2] = fabs(ent->movedir[2]);
|
|
ent->moveinfo.distance = abs_movedir[0] * ent->size[0] + abs_movedir[1] *
|
|
ent->size[1] + abs_movedir[2] * ent->size[2] -
|
|
st.lip;
|
|
VectorMA(ent->pos1, ent->moveinfo.distance, ent->movedir, ent->pos2);
|
|
|
|
/* if it starts open, switch the positions */
|
|
if (ent->spawnflags & DOOR_START_OPEN)
|
|
{
|
|
VectorCopy(ent->pos2, ent->s.origin);
|
|
VectorCopy(ent->pos1, ent->pos2);
|
|
VectorCopy(ent->s.origin, ent->pos1);
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_BOTTOM;
|
|
|
|
if (ent->health)
|
|
{
|
|
ent->takedamage = DAMAGE_YES;
|
|
ent->die = door_killed;
|
|
ent->max_health = ent->health;
|
|
}
|
|
else if (ent->targetname && ent->message)
|
|
{
|
|
gi.soundindex("misc/talk.wav");
|
|
ent->touch = door_touch;
|
|
}
|
|
|
|
ent->moveinfo.speed = ent->speed;
|
|
ent->moveinfo.accel = ent->accel;
|
|
ent->moveinfo.decel = ent->decel;
|
|
ent->moveinfo.wait = ent->wait;
|
|
VectorCopy(ent->pos1, ent->moveinfo.start_origin);
|
|
VectorCopy(ent->s.angles, ent->moveinfo.start_angles);
|
|
VectorCopy(ent->pos2, ent->moveinfo.end_origin);
|
|
VectorCopy(ent->s.angles, ent->moveinfo.end_angles);
|
|
|
|
if (ent->spawnflags & 16)
|
|
{
|
|
ent->s.effects |= EF_ANIM_ALL;
|
|
}
|
|
|
|
if (ent->spawnflags & 64)
|
|
{
|
|
ent->s.effects |= EF_ANIM_ALLFAST;
|
|
}
|
|
|
|
/* to simplify logic elsewhere, make
|
|
non-teamed doors into a team of one */
|
|
if (!ent->team)
|
|
{
|
|
ent->teammaster = ent;
|
|
}
|
|
|
|
gi.linkentity(ent);
|
|
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
|
|
if (ent->health || ent->targetname)
|
|
{
|
|
ent->think = Think_CalcMoveSpeed;
|
|
}
|
|
else
|
|
{
|
|
ent->think = Think_SpawnDoorTrigger;
|
|
}
|
|
}
|
|
|
|
void
|
|
Door_Activate(edict_t *self, edict_t *other /* unused */,
|
|
edict_t *activator /* unused */)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->use = NULL;
|
|
|
|
if (self->health)
|
|
{
|
|
self->takedamage = DAMAGE_YES;
|
|
self->die = door_killed;
|
|
self->max_health = self->health;
|
|
}
|
|
|
|
if (self->health)
|
|
{
|
|
self->think = Think_CalcMoveSpeed;
|
|
}
|
|
else
|
|
{
|
|
self->think = Think_SpawnDoorTrigger;
|
|
}
|
|
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
/*
|
|
* QUAKED func_door_rotating (0 .5 .8) ? START_OPEN REVERSE CRUSHER NOMONSTER ANIMATED TOGGLE X_AXIS Y_AXIS
|
|
*
|
|
* TOGGLE causes the door to wait in both the start and end states for a trigger event.
|
|
* START_OPEN the door to moves to its destination when spawned, and operate in reverse.
|
|
* It is used to temporarily or permanently close off an area when triggered
|
|
* (not useful for touch or takedamage doors).
|
|
* NOMONSTER monsters will not trigger this door
|
|
*
|
|
* You need to have an origin brush as part of this entity. The center of that brush will be
|
|
* the point around which it is rotated. It will rotate around the Z axis by default. You can
|
|
* check either the X_AXIS or Y_AXIS box to change that.
|
|
*
|
|
* "distance" is how many degrees the door will be rotated.
|
|
* "speed" determines how fast the door moves; default value is 100.
|
|
*
|
|
* REVERSE will cause the door to rotate in the opposite direction.
|
|
*
|
|
* "message" is printed when the door is touched if it is a trigger door and it hasn't been fired yet
|
|
* "angle" determines the opening direction
|
|
* "targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door.
|
|
* "health" if set, door must be shot open
|
|
* "speed" movement speed (100 default)
|
|
* "wait" wait before returning (3 default, -1 = never return)
|
|
* "dmg" damage to inflict when blocked (2 default)
|
|
* "sounds"
|
|
* 1) silent
|
|
* 2) light
|
|
* 3) medium
|
|
* 4) heavy
|
|
*/
|
|
void
|
|
SP_func_door_rotating(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorClear(ent->s.angles);
|
|
|
|
/* set the axis of rotation */
|
|
VectorClear(ent->movedir);
|
|
|
|
if (ent->spawnflags & DOOR_X_AXIS)
|
|
{
|
|
ent->movedir[2] = 1.0;
|
|
}
|
|
else if (ent->spawnflags & DOOR_Y_AXIS)
|
|
{
|
|
ent->movedir[0] = 1.0;
|
|
}
|
|
else /* Z_AXIS */
|
|
{
|
|
ent->movedir[1] = 1.0;
|
|
}
|
|
|
|
/* check for reverse rotation */
|
|
if (ent->spawnflags & DOOR_REVERSE)
|
|
{
|
|
VectorNegate(ent->movedir, ent->movedir);
|
|
}
|
|
|
|
if (!st.distance)
|
|
{
|
|
gi.dprintf("%s at %s with no distance set\n", ent->classname,
|
|
vtos(ent->s.origin));
|
|
st.distance = 90;
|
|
}
|
|
|
|
VectorCopy(ent->s.angles, ent->pos1);
|
|
VectorMA(ent->s.angles, st.distance, ent->movedir, ent->pos2);
|
|
ent->moveinfo.distance = st.distance;
|
|
|
|
ent->movetype = MOVETYPE_PUSH;
|
|
ent->solid = SOLID_BSP;
|
|
gi.setmodel(ent, ent->model);
|
|
|
|
ent->blocked = door_blocked;
|
|
ent->use = door_use;
|
|
|
|
if (!ent->speed)
|
|
{
|
|
ent->speed = 100;
|
|
}
|
|
|
|
if (!ent->accel)
|
|
{
|
|
ent->accel = ent->speed;
|
|
}
|
|
|
|
if (!ent->decel)
|
|
{
|
|
ent->decel = ent->speed;
|
|
}
|
|
|
|
if (!ent->wait)
|
|
{
|
|
ent->wait = 3;
|
|
}
|
|
|
|
if (!ent->dmg)
|
|
{
|
|
ent->dmg = 2;
|
|
}
|
|
|
|
if (ent->sounds != 1)
|
|
{
|
|
ent->moveinfo.sound_start = gi.soundindex("doors/dr1_strt.wav");
|
|
ent->moveinfo.sound_middle = gi.soundindex("doors/dr1_mid.wav");
|
|
ent->moveinfo.sound_end = gi.soundindex("doors/dr1_end.wav");
|
|
}
|
|
|
|
/* if it starts open, switch the positions */
|
|
if (ent->spawnflags & DOOR_START_OPEN)
|
|
{
|
|
VectorCopy(ent->pos2, ent->s.angles);
|
|
VectorCopy(ent->pos1, ent->pos2);
|
|
VectorCopy(ent->s.angles, ent->pos1);
|
|
VectorNegate(ent->movedir, ent->movedir);
|
|
}
|
|
|
|
if (ent->health)
|
|
{
|
|
ent->takedamage = DAMAGE_YES;
|
|
ent->die = door_killed;
|
|
ent->max_health = ent->health;
|
|
}
|
|
|
|
if (ent->targetname && ent->message)
|
|
{
|
|
gi.soundindex("misc/talk.wav");
|
|
ent->touch = door_touch;
|
|
}
|
|
|
|
ent->moveinfo.state = STATE_BOTTOM;
|
|
ent->moveinfo.speed = ent->speed;
|
|
ent->moveinfo.accel = ent->accel;
|
|
ent->moveinfo.decel = ent->decel;
|
|
ent->moveinfo.wait = ent->wait;
|
|
VectorCopy(ent->s.origin, ent->moveinfo.start_origin);
|
|
VectorCopy(ent->pos1, ent->moveinfo.start_angles);
|
|
VectorCopy(ent->s.origin, ent->moveinfo.end_origin);
|
|
VectorCopy(ent->pos2, ent->moveinfo.end_angles);
|
|
|
|
if (ent->spawnflags & 16)
|
|
{
|
|
ent->s.effects |= EF_ANIM_ALL;
|
|
}
|
|
|
|
/* to simplify logic elsewhere, make non-teamed doors into a team of one */
|
|
if (!ent->team)
|
|
{
|
|
ent->teammaster = ent;
|
|
}
|
|
|
|
gi.linkentity(ent);
|
|
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
|
|
if (ent->health || ent->targetname)
|
|
{
|
|
ent->think = Think_CalcMoveSpeed;
|
|
}
|
|
else
|
|
{
|
|
ent->think = Think_SpawnDoorTrigger;
|
|
}
|
|
|
|
if (ent->spawnflags & DOOR_INACTIVE)
|
|
{
|
|
ent->takedamage = DAMAGE_NO;
|
|
ent->die = NULL;
|
|
ent->think = NULL;
|
|
ent->nextthink = 0;
|
|
ent->use = Door_Activate;
|
|
}
|
|
}
|
|
|
|
void
|
|
smart_water_blocked(edict_t *self, edict_t *other)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(other->svflags & SVF_MONSTER) && (!other->client))
|
|
{
|
|
/* give it a chance to go away on it's own terms (like gibs) */
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, 100000, 1, 0, MOD_LAVA);
|
|
|
|
/* if it's still there, nuke it */
|
|
if (other->inuse)
|
|
{
|
|
BecomeExplosion1(other);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, 100, 1, 0, MOD_LAVA);
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* QUAKED func_water (0 .5 .8) ? START_OPEN
|
|
*
|
|
* func_water is a moveable water brush. It must be targeted to operate.
|
|
* Use a non-water texture at your own risk.
|
|
*
|
|
* START_OPEN causes the water to move to its destination when spawned
|
|
* and operate in reverse.
|
|
*
|
|
* "angle" determines the opening direction (up or down only)
|
|
* "speed" movement speed (25 default)
|
|
* "wait" wait before returning (-1 default, -1 = TOGGLE)
|
|
* "lip" lip remaining at end of move (0 default)
|
|
* "sounds" (yes, these need to be changed)
|
|
* 0) no sound
|
|
* 1) water
|
|
* 2) lava
|
|
*/
|
|
void
|
|
SP_func_water(edict_t *self)
|
|
{
|
|
vec3_t abs_movedir;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
G_SetMovedir(self->s.angles, self->movedir);
|
|
self->movetype = MOVETYPE_PUSH;
|
|
self->solid = SOLID_BSP;
|
|
gi.setmodel(self, self->model);
|
|
|
|
switch (self->sounds)
|
|
{
|
|
default:
|
|
break;
|
|
|
|
case 1: /* water */
|
|
case 2: /* lava */
|
|
self->moveinfo.sound_start = gi.soundindex("world/mov_watr.wav");
|
|
self->moveinfo.sound_end = gi.soundindex("world/stp_watr.wav");
|
|
break;
|
|
}
|
|
|
|
/* calculate second position */
|
|
VectorCopy(self->s.origin, self->pos1);
|
|
abs_movedir[0] = fabs(self->movedir[0]);
|
|
abs_movedir[1] = fabs(self->movedir[1]);
|
|
abs_movedir[2] = fabs(self->movedir[2]);
|
|
self->moveinfo.distance = abs_movedir[0] * self->size[0] + abs_movedir[1] *
|
|
self->size[1] + abs_movedir[2] * self->size[2] -
|
|
st.lip;
|
|
VectorMA(self->pos1, self->moveinfo.distance, self->movedir, self->pos2);
|
|
|
|
/* if it starts open, switch the positions */
|
|
if (self->spawnflags & DOOR_START_OPEN)
|
|
{
|
|
VectorCopy(self->pos2, self->s.origin);
|
|
VectorCopy(self->pos1, self->pos2);
|
|
VectorCopy(self->s.origin, self->pos1);
|
|
}
|
|
|
|
VectorCopy(self->pos1, self->moveinfo.start_origin);
|
|
VectorCopy(self->s.angles, self->moveinfo.start_angles);
|
|
VectorCopy(self->pos2, self->moveinfo.end_origin);
|
|
VectorCopy(self->s.angles, self->moveinfo.end_angles);
|
|
|
|
self->moveinfo.state = STATE_BOTTOM;
|
|
|
|
if (!self->speed)
|
|
{
|
|
self->speed = 25;
|
|
}
|
|
|
|
self->moveinfo.accel = self->moveinfo.decel = self->moveinfo.speed = self->speed;
|
|
|
|
if (self->spawnflags & 2) /* smart water */
|
|
{
|
|
if (!self->accel)
|
|
{
|
|
self->accel = 20;
|
|
}
|
|
|
|
self->blocked = smart_water_blocked;
|
|
}
|
|
|
|
if (!self->wait)
|
|
{
|
|
self->wait = -1;
|
|
}
|
|
|
|
self->moveinfo.wait = self->wait;
|
|
|
|
self->use = door_use;
|
|
|
|
if (self->wait == -1)
|
|
{
|
|
self->spawnflags |= DOOR_TOGGLE;
|
|
}
|
|
|
|
self->classname = "func_door";
|
|
|
|
gi.linkentity(self);
|
|
}
|
|
|
|
/*
|
|
* QUAKED func_train (0 .5 .8) ? START_ON TOGGLE BLOCK_STOPS
|
|
*
|
|
* Trains are moving platforms that players can ride.
|
|
* The targets origin specifies the min point of the train
|
|
* at each corner. The train spawns at the first target it
|
|
* is pointing at. If the train is the target of a button
|
|
* or trigger, it will not begin moving until activated.
|
|
*
|
|
* speed default 100
|
|
* dmg default 2
|
|
* noise looping sound to play when the train is in motion
|
|
*
|
|
*/
|
|
void
|
|
train_blocked(edict_t *self, edict_t *other)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(other->svflags & SVF_MONSTER) && (!other->client))
|
|
{
|
|
/* give it a chance to go away on it's own terms (like gibs) */
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, 100000, 1, 0, MOD_CRUSH);
|
|
|
|
/* if it's still there, nuke it */
|
|
if (other->inuse)
|
|
{
|
|
/* Hack for entity without an origin near the model */
|
|
VectorMA(other->absmin, 0.5, other->size, other->s.origin);
|
|
BecomeExplosion1(other);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (level.time < self->touch_debounce_time)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!self->dmg)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->touch_debounce_time = level.time + 0.5;
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
|
|
}
|
|
|
|
void
|
|
train_wait(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->target_ent->pathtarget)
|
|
{
|
|
char *savetarget;
|
|
edict_t *ent;
|
|
|
|
ent = self->target_ent;
|
|
savetarget = ent->target;
|
|
ent->target = ent->pathtarget;
|
|
G_UseTargets(ent, self->activator);
|
|
ent->target = savetarget;
|
|
|
|
/* make sure we didn't get killed by a killtarget */
|
|
if (!self->inuse)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (self->moveinfo.wait)
|
|
{
|
|
if (self->moveinfo.wait > 0)
|
|
{
|
|
self->nextthink = level.time + self->moveinfo.wait;
|
|
self->think = train_next;
|
|
}
|
|
else if (self->spawnflags & TRAIN_TOGGLE)
|
|
{
|
|
self->target_ent = NULL;
|
|
self->spawnflags &= ~TRAIN_START_ON;
|
|
VectorClear(self->velocity);
|
|
self->nextthink = 0;
|
|
}
|
|
|
|
if (!(self->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (self->moveinfo.sound_end)
|
|
{
|
|
gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
self->moveinfo.sound_end,
|
|
1, ATTN_STATIC, 0);
|
|
}
|
|
|
|
self->s.sound = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
train_next(self);
|
|
}
|
|
}
|
|
|
|
void
|
|
train_piece_wait(edict_t *self)
|
|
{
|
|
}
|
|
|
|
void
|
|
train_next(edict_t *self)
|
|
{
|
|
edict_t *ent;
|
|
vec3_t dest;
|
|
qboolean first;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
first = true;
|
|
|
|
again:
|
|
if (!self->target)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ent = G_PickTarget(self->target);
|
|
|
|
if (!ent)
|
|
{
|
|
gi.dprintf("train_next: bad target %s\n", self->target);
|
|
return;
|
|
}
|
|
|
|
self->target = ent->target;
|
|
|
|
/* check for a teleport path_corner */
|
|
if (ent->spawnflags & 1)
|
|
{
|
|
if (!first)
|
|
{
|
|
gi.dprintf("connected teleport path_corners, see %s at %s\n",
|
|
ent->classname, vtos(ent->s.origin));
|
|
return;
|
|
}
|
|
|
|
first = false;
|
|
VectorSubtract(ent->s.origin, self->mins, self->s.origin);
|
|
VectorCopy(self->s.origin, self->s.old_origin);
|
|
self->s.event = EV_OTHER_TELEPORT;
|
|
gi.linkentity(self);
|
|
goto again;
|
|
}
|
|
|
|
if (ent->speed)
|
|
{
|
|
self->speed = ent->speed;
|
|
self->moveinfo.speed = ent->speed;
|
|
|
|
if (ent->accel)
|
|
{
|
|
self->moveinfo.accel = ent->accel;
|
|
}
|
|
else
|
|
{
|
|
self->moveinfo.accel = ent->speed;
|
|
}
|
|
|
|
if (ent->decel)
|
|
{
|
|
self->moveinfo.decel = ent->decel;
|
|
}
|
|
else
|
|
{
|
|
self->moveinfo.decel = ent->speed;
|
|
}
|
|
|
|
self->moveinfo.current_speed = 0;
|
|
}
|
|
|
|
self->moveinfo.wait = ent->wait;
|
|
self->target_ent = ent;
|
|
|
|
if (!(self->flags & FL_TEAMSLAVE))
|
|
{
|
|
if (self->moveinfo.sound_start)
|
|
{
|
|
gi.sound(self, CHAN_NO_PHS_ADD + CHAN_VOICE,
|
|
self->moveinfo.sound_start, 1,
|
|
ATTN_STATIC, 0);
|
|
}
|
|
|
|
self->s.sound = self->moveinfo.sound_middle;
|
|
}
|
|
|
|
VectorSubtract(ent->s.origin, self->mins, dest);
|
|
self->moveinfo.state = STATE_TOP;
|
|
VectorCopy(self->s.origin, self->moveinfo.start_origin);
|
|
VectorCopy(dest, self->moveinfo.end_origin);
|
|
Move_Calc(self, dest, train_wait);
|
|
self->spawnflags |= TRAIN_START_ON;
|
|
|
|
if (self->team)
|
|
{
|
|
edict_t *e;
|
|
vec3_t dir, dst;
|
|
|
|
VectorSubtract(dest, self->s.origin, dir);
|
|
|
|
for (e = self->teamchain; e; e = e->teamchain)
|
|
{
|
|
VectorAdd(dir, e->s.origin, dst);
|
|
VectorCopy(e->s.origin, e->moveinfo.start_origin);
|
|
VectorCopy(dst, e->moveinfo.end_origin);
|
|
|
|
e->moveinfo.state = STATE_TOP;
|
|
e->speed = self->speed;
|
|
e->moveinfo.speed = self->moveinfo.speed;
|
|
e->moveinfo.accel = self->moveinfo.accel;
|
|
e->moveinfo.decel = self->moveinfo.decel;
|
|
e->movetype = MOVETYPE_PUSH;
|
|
Move_Calc(e, dst, train_piece_wait);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
train_resume(edict_t *self)
|
|
{
|
|
edict_t *ent;
|
|
vec3_t dest;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ent = self->target_ent;
|
|
|
|
VectorSubtract(ent->s.origin, self->mins, dest);
|
|
self->moveinfo.state = STATE_TOP;
|
|
VectorCopy(self->s.origin, self->moveinfo.start_origin);
|
|
VectorCopy(dest, self->moveinfo.end_origin);
|
|
Move_Calc(self, dest, train_wait);
|
|
self->spawnflags |= TRAIN_START_ON;
|
|
}
|
|
|
|
void
|
|
func_train_find(edict_t *self)
|
|
{
|
|
edict_t *ent;
|
|
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!self->target)
|
|
{
|
|
gi.dprintf("train_find: no target\n");
|
|
return;
|
|
}
|
|
|
|
ent = G_PickTarget(self->target);
|
|
|
|
if (!ent)
|
|
{
|
|
gi.dprintf("train_find: target %s not found\n", self->target);
|
|
return;
|
|
}
|
|
|
|
self->target = ent->target;
|
|
|
|
VectorSubtract(ent->s.origin, self->mins, self->s.origin);
|
|
gi.linkentity(self);
|
|
|
|
/* if not triggered, start immediately */
|
|
if (!self->targetname)
|
|
{
|
|
self->spawnflags |= TRAIN_START_ON;
|
|
}
|
|
|
|
if (self->spawnflags & TRAIN_START_ON)
|
|
{
|
|
self->nextthink = level.time + FRAMETIME;
|
|
self->think = train_next;
|
|
self->activator = self;
|
|
}
|
|
}
|
|
|
|
void
|
|
train_use(edict_t *self, edict_t *other /* unused */,
|
|
edict_t *activator)
|
|
{
|
|
if (!self || !activator)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->activator = activator;
|
|
|
|
if (self->spawnflags & TRAIN_START_ON)
|
|
{
|
|
if (!(self->spawnflags & TRAIN_TOGGLE))
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->spawnflags &= ~TRAIN_START_ON;
|
|
VectorClear(self->velocity);
|
|
self->nextthink = 0;
|
|
}
|
|
else
|
|
{
|
|
if (self->target_ent)
|
|
{
|
|
train_resume(self);
|
|
}
|
|
else
|
|
{
|
|
train_next(self);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
SP_func_train(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->movetype = MOVETYPE_PUSH;
|
|
|
|
VectorClear(self->s.angles);
|
|
self->blocked = train_blocked;
|
|
|
|
if (self->spawnflags & TRAIN_BLOCK_STOPS)
|
|
{
|
|
self->dmg = 0;
|
|
}
|
|
else
|
|
{
|
|
if (!self->dmg)
|
|
{
|
|
self->dmg = 100;
|
|
}
|
|
}
|
|
|
|
self->solid = SOLID_BSP;
|
|
gi.setmodel(self, self->model);
|
|
|
|
if (st.noise)
|
|
{
|
|
self->moveinfo.sound_middle = gi.soundindex(st.noise);
|
|
}
|
|
|
|
if (!self->speed)
|
|
{
|
|
self->speed = 100;
|
|
}
|
|
|
|
self->moveinfo.speed = self->speed;
|
|
self->moveinfo.accel = self->moveinfo.decel = self->moveinfo.speed;
|
|
|
|
self->use = train_use;
|
|
|
|
gi.linkentity(self);
|
|
|
|
if (self->target)
|
|
{
|
|
/* start trains on the second frame, to make
|
|
sure their targets have had a chance to spawn */
|
|
self->nextthink = level.time + FRAMETIME;
|
|
self->think = func_train_find;
|
|
}
|
|
else
|
|
{
|
|
gi.dprintf("func_train without a target at %s\n", vtos(self->absmin));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* QUAKED trigger_elevator (0.3 0.1 0.6) (-8 -8 -8) (8 8 8)
|
|
*/
|
|
void
|
|
trigger_elevator_use(edict_t *self, edict_t *other,
|
|
edict_t *activator /* unused */)
|
|
{
|
|
edict_t *target;
|
|
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->movetarget->nextthink)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!other->pathtarget)
|
|
{
|
|
gi.dprintf("elevator used with no pathtarget\n");
|
|
return;
|
|
}
|
|
|
|
target = G_PickTarget(other->pathtarget);
|
|
|
|
if (!target)
|
|
{
|
|
gi.dprintf("elevator used with bad pathtarget: %s\n",
|
|
other->pathtarget);
|
|
return;
|
|
}
|
|
|
|
self->movetarget->target_ent = target;
|
|
train_resume(self->movetarget);
|
|
}
|
|
|
|
void
|
|
trigger_elevator_init(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!self->target)
|
|
{
|
|
gi.dprintf("trigger_elevator has no target\n");
|
|
return;
|
|
}
|
|
|
|
self->movetarget = G_PickTarget(self->target);
|
|
|
|
if (!self->movetarget)
|
|
{
|
|
gi.dprintf("trigger_elevator unable to find target %s\n", self->target);
|
|
return;
|
|
}
|
|
|
|
if (strcmp(self->movetarget->classname, "func_train") != 0)
|
|
{
|
|
gi.dprintf("trigger_elevator target %s is not a train\n", self->target);
|
|
return;
|
|
}
|
|
|
|
self->use = trigger_elevator_use;
|
|
self->svflags = SVF_NOCLIENT;
|
|
}
|
|
|
|
void
|
|
SP_trigger_elevator(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->think = trigger_elevator_init;
|
|
self->nextthink = level.time + FRAMETIME;
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* QUAKED func_timer (0.3 0.1 0.6) (-8 -8 -8) (8 8 8) START_ON
|
|
*
|
|
* "wait" base time between triggering all targets, default is 1
|
|
* "random" wait variance, default is 0
|
|
*
|
|
* so, the basic time between firing is a random time
|
|
* between (wait - random) and (wait + random)
|
|
*
|
|
* "delay" delay before first firing when turned on, default is 0
|
|
* "pausetime" additional delay used only the very first time
|
|
* and only if spawned with START_ON
|
|
*
|
|
* These can used but not touched.
|
|
*/
|
|
void
|
|
func_timer_think(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
G_UseTargets(self, self->activator);
|
|
self->nextthink = level.time + self->wait + crandom() * self->random;
|
|
}
|
|
|
|
void
|
|
func_timer_use(edict_t *self, edict_t *other /* unused */, edict_t *activator)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->activator = activator;
|
|
|
|
/* if on, turn it off */
|
|
if (self->nextthink)
|
|
{
|
|
self->nextthink = 0;
|
|
return;
|
|
}
|
|
|
|
/* turn it on */
|
|
if (self->delay)
|
|
{
|
|
self->nextthink = level.time + self->delay;
|
|
}
|
|
else
|
|
{
|
|
func_timer_think(self);
|
|
}
|
|
}
|
|
|
|
void
|
|
SP_func_timer(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!self->wait)
|
|
{
|
|
self->wait = 1.0;
|
|
}
|
|
|
|
self->use = func_timer_use;
|
|
self->think = func_timer_think;
|
|
|
|
if (self->random >= self->wait)
|
|
{
|
|
self->random = self->wait - FRAMETIME;
|
|
gi.dprintf("func_timer at %s has random >= wait\n",
|
|
vtos(self->s.origin));
|
|
}
|
|
|
|
if (self->spawnflags & 1)
|
|
{
|
|
self->nextthink = level.time + 1.0 + st.pausetime + self->delay +
|
|
self->wait + crandom() * self->random;
|
|
self->activator = self;
|
|
}
|
|
|
|
self->svflags = SVF_NOCLIENT;
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* QUAKED func_conveyor (0 .5 .8) ? START_ON TOGGLE
|
|
*
|
|
* Conveyors are stationary brushes that move what's on them.
|
|
* The brush should be have a surface with at least one current
|
|
* content enabled.
|
|
*
|
|
* speed default 100
|
|
*/
|
|
void
|
|
func_conveyor_use(edict_t *self, edict_t *other /* unused */,
|
|
edict_t *activator /* unused */)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->spawnflags & 1)
|
|
{
|
|
self->speed = 0;
|
|
self->spawnflags &= ~1;
|
|
}
|
|
else
|
|
{
|
|
self->speed = self->count;
|
|
self->spawnflags |= 1;
|
|
}
|
|
|
|
if (!(self->spawnflags & 2))
|
|
{
|
|
self->count = 0;
|
|
}
|
|
}
|
|
|
|
void
|
|
SP_func_conveyor(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!self->speed)
|
|
{
|
|
self->speed = 100;
|
|
}
|
|
|
|
if (!(self->spawnflags & 1))
|
|
{
|
|
self->count = self->speed;
|
|
self->speed = 0;
|
|
}
|
|
|
|
self->use = func_conveyor_use;
|
|
|
|
gi.setmodel(self, self->model);
|
|
self->solid = SOLID_BSP;
|
|
gi.linkentity(self);
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* QUAKED func_door_secret (0 .5 .8) ? always_shoot 1st_left 1st_down
|
|
* A secret door. Slide back and then to the side.
|
|
*
|
|
* open_once doors never closes
|
|
* 1st_left 1st move is left of arrow
|
|
* 1st_down 1st move is down from arrow
|
|
* always_shoot door is shootebale even if targeted
|
|
*
|
|
* "angle" determines the direction
|
|
* "dmg" damage to inflic when blocked (default 2)
|
|
* "wait" how long to hold in the open position (default 5, -1 means hold)
|
|
*/
|
|
void
|
|
door_secret_use(edict_t *self, edict_t *other /* unused */,
|
|
edict_t *activator /*unused */)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* make sure we're not already moving */
|
|
if (!VectorCompare(self->s.origin, vec3_origin))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Move_Calc(self, self->pos1, door_secret_move1);
|
|
door_use_areaportals(self, true);
|
|
}
|
|
|
|
void
|
|
door_secret_move1(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->nextthink = level.time + 1.0;
|
|
self->think = door_secret_move2;
|
|
}
|
|
|
|
void
|
|
door_secret_move2(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Move_Calc(self, self->pos2, door_secret_move3);
|
|
}
|
|
|
|
void
|
|
door_secret_move3(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (self->wait == -1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->nextthink = level.time + self->wait;
|
|
self->think = door_secret_move4;
|
|
}
|
|
|
|
void
|
|
door_secret_move4(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Move_Calc(self, self->pos1, door_secret_move5);
|
|
}
|
|
|
|
void
|
|
door_secret_move5(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->nextthink = level.time + 1.0;
|
|
self->think = door_secret_move6;
|
|
}
|
|
|
|
void
|
|
door_secret_move6(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Move_Calc(self, vec3_origin, door_secret_done);
|
|
}
|
|
|
|
void
|
|
door_secret_done(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(self->targetname) || (self->spawnflags & SECRET_ALWAYS_SHOOT))
|
|
{
|
|
self->health = 0;
|
|
self->takedamage = DAMAGE_YES;
|
|
}
|
|
|
|
door_use_areaportals(self, false);
|
|
}
|
|
|
|
void
|
|
door_secret_blocked(edict_t *self, edict_t *other)
|
|
{
|
|
if (!self || !other)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(other->svflags & SVF_MONSTER) && (!other->client))
|
|
{
|
|
/* give it a chance to go away on it's own terms (like gibs) */
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, 100000, 1, 0, MOD_CRUSH);
|
|
|
|
/* if it's still there, nuke it */
|
|
if (other->inuse)
|
|
{
|
|
/* Hack for entities without their origin near the model */
|
|
VectorMA(other->absmin, 0.5, other->size, other->s.origin);
|
|
BecomeExplosion1(other);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (level.time < self->touch_debounce_time)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->touch_debounce_time = level.time + 0.5;
|
|
|
|
T_Damage(other, self, self, vec3_origin, other->s.origin,
|
|
vec3_origin, self->dmg, 1, 0, MOD_CRUSH);
|
|
}
|
|
|
|
void
|
|
door_secret_die(edict_t *self, edict_t *inflictor /* unused */,
|
|
edict_t *attacker, int damage /* unused */,
|
|
vec3_t point /* unused */)
|
|
{
|
|
if (!self || !attacker)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self->takedamage = DAMAGE_NO;
|
|
door_secret_use(self, attacker, attacker);
|
|
}
|
|
|
|
void
|
|
SP_func_door_secret(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
vec3_t forward, right, up;
|
|
float side;
|
|
float width;
|
|
float length;
|
|
|
|
ent->moveinfo.sound_start = gi.soundindex("doors/dr1_strt.wav");
|
|
ent->moveinfo.sound_middle = gi.soundindex("doors/dr1_mid.wav");
|
|
ent->moveinfo.sound_end = gi.soundindex("doors/dr1_end.wav");
|
|
|
|
ent->movetype = MOVETYPE_PUSH;
|
|
ent->solid = SOLID_BSP;
|
|
gi.setmodel(ent, ent->model);
|
|
|
|
ent->blocked = door_secret_blocked;
|
|
ent->use = door_secret_use;
|
|
|
|
if (!(ent->targetname) || (ent->spawnflags & SECRET_ALWAYS_SHOOT))
|
|
{
|
|
ent->health = 0;
|
|
ent->takedamage = DAMAGE_YES;
|
|
ent->die = door_secret_die;
|
|
}
|
|
|
|
if (!ent->dmg)
|
|
{
|
|
ent->dmg = 2;
|
|
}
|
|
|
|
if (!ent->wait)
|
|
{
|
|
ent->wait = 5;
|
|
}
|
|
|
|
ent->moveinfo.accel = ent->moveinfo.decel =
|
|
ent->moveinfo.speed = 50;
|
|
|
|
/* calculate positions */
|
|
AngleVectors(ent->s.angles, forward, right, up);
|
|
VectorClear(ent->s.angles);
|
|
side = 1.0 - (ent->spawnflags & SECRET_1ST_LEFT);
|
|
|
|
if (ent->spawnflags & SECRET_1ST_DOWN)
|
|
{
|
|
width = fabs(DotProduct(up, ent->size));
|
|
}
|
|
else
|
|
{
|
|
width = fabs(DotProduct(right, ent->size));
|
|
}
|
|
|
|
length = fabs(DotProduct(forward, ent->size));
|
|
|
|
if (ent->spawnflags & SECRET_1ST_DOWN)
|
|
{
|
|
VectorMA(ent->s.origin, -1 * width, up, ent->pos1);
|
|
}
|
|
else
|
|
{
|
|
VectorMA(ent->s.origin, side * width, right, ent->pos1);
|
|
}
|
|
|
|
VectorMA(ent->pos1, length, forward, ent->pos2);
|
|
|
|
if (ent->health)
|
|
{
|
|
ent->takedamage = DAMAGE_YES;
|
|
ent->die = door_killed;
|
|
ent->max_health = ent->health;
|
|
}
|
|
else if (ent->targetname && ent->message)
|
|
{
|
|
gi.soundindex("misc/talk.wav");
|
|
ent->touch = door_touch;
|
|
}
|
|
|
|
ent->classname = "func_door";
|
|
|
|
gi.linkentity(ent);
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* QUAKED func_killbox (1 0 0) ?
|
|
*
|
|
* Kills everything inside when fired,
|
|
* irrespective of protection.
|
|
*/
|
|
void
|
|
use_killbox(edict_t *self, edict_t *other /* unused */,
|
|
edict_t *activator /* unused */)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
KillBox(self);
|
|
|
|
/* Hack to make sure that really everything is killed */
|
|
self->count--;
|
|
|
|
if (!self->count)
|
|
{
|
|
self->think = G_FreeEdict;
|
|
self->nextthink = level.time + 1;
|
|
}
|
|
}
|
|
|
|
void
|
|
SP_func_killbox(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
gi.setmodel(ent, ent->model);
|
|
ent->use = use_killbox;
|
|
ent->svflags = SVF_NOCLIENT;
|
|
}
|