mirror of
https://github.com/yquake2/yquake2remaster.git
synced 2025-02-20 10:53:22 +00:00
there's a target_secret (with targetname "t117"), but no one triggers it - that's why the help computer shows four secrets, but you can only get three of them. Now when you open the door in front of the hidden secret armor (by shooting it), it'll trigger the target_secret and you can get all four secrets. fixes #182
2995 lines
56 KiB
C
2995 lines
56 KiB
C
/*
|
|
* Copyright (C) 1997-2001 Id Software, Inc.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or (at
|
|
* your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
*
|
|
* See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
|
|
* 02111-1307, USA.
|
|
*
|
|
* =======================================================================
|
|
*
|
|
* Level functions. Platforms, buttons, dooors and so on.
|
|
*
|
|
* =======================================================================
|
|
*/
|
|
|
|
#include "header/local.h"
|
|
|
|
/*
|
|
* =========================================================
|
|
*
|
|
* 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
|
|
*
|
|
*
|
|
* =========================================================
|
|
*/
|
|
|
|
#define PLAT_LOW_TRIGGER 1
|
|
|
|
#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
|
|
|
|
/* Support routines for movement (changes in origin using velocity) */
|
|
|
|
void Think_AccelMove(edict_t *ent);
|
|
void plat_go_down(edict_t *ent);
|
|
|
|
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)
|
|
{
|
|
float frames;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* 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);
|
|
|
|
/* set nextthink to trigger a think when dest is reached */
|
|
ent->nextthink = level.time + frames * FRAMETIME;
|
|
ent->think = AngleMove_Final;
|
|
}
|
|
|
|
void
|
|
AngleMove_Calc(edict_t *ent, void (*func)(edict_t *))
|
|
{
|
|
if (!ent || !func)
|
|
{
|
|
return;
|
|
}
|
|
|
|
VectorClear(ent->avelocity);
|
|
ent->moveinfo.endfunc = func;
|
|
|
|
if (level.current_entity ==
|
|
((ent->flags & FL_TEAMSLAVE) ? ent->teammaster : ent))
|
|
{
|
|
AngleMove_Begin(ent);
|
|
}
|
|
else
|
|
{
|
|
ent->nextthink = level.time + FRAMETIME;
|
|
ent->think = AngleMove_Begin;
|
|
}
|
|
}
|
|
|
|
#define AccelerationDistance(target, rate) (target * ((target / rate) + 1) / 2)
|
|
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* The team has completed a frame of movement,
|
|
* so change the speed for the next frame
|
|
*/
|
|
void
|
|
Think_AccelMove(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ent->moveinfo.remaining_distance -= ent->moveinfo.current_speed;
|
|
|
|
if (ent->moveinfo.current_speed == 0) /* starting or blocked */
|
|
{
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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)
|
|
{
|
|
/* Hack for entity without it's 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->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 /* unused */, edict_t *activator /* unused */)
|
|
{
|
|
if (!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 /* unused */,
|
|
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)
|
|
{
|
|
/* the player is still on the plat, so delay going down */
|
|
ent->nextthink = level.time + 1;
|
|
}
|
|
}
|
|
|
|
void
|
|
plat_spawn_inside_trigger(edict_t *ent)
|
|
{
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
edict_t *trigger;
|
|
vec3_t tmin, tmax;
|
|
|
|
/* 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;
|
|
tmin[2] = ent->mins[2];
|
|
|
|
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);
|
|
}
|
|
|
|
/*
|
|
* 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");
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* QUAKED func_rotating (0 .5 .8) ? START_ON REVERSE X_AXIS Y_AXIS
|
|
* TOUCH_PAIN STOP ANIMATED ANIMATED_FAST
|
|
*
|
|
* 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.
|
|
*
|
|
* "speed" determines how fast it moves; default value is 100.
|
|
* "dmg" damage to inflict when blocked (2 default)
|
|
*
|
|
* REVERSE will cause the it to rotate in the opposite direction.
|
|
* STOP mean it will stop moving instead of pushing entities
|
|
*/
|
|
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;
|
|
VectorClear(self->avelocity);
|
|
self->touch = NULL;
|
|
}
|
|
else
|
|
{
|
|
self->s.sound = self->moveinfo.sound_middle;
|
|
VectorScale(self->movedir, self->speed, self->avelocity);
|
|
|
|
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;
|
|
|
|
if (ent->dmg)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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 /* unsued */,
|
|
int damage /* unused */, vec3_t point /* unused */)
|
|
{
|
|
if (!self)
|
|
{
|
|
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
|
|
*/
|
|
|
|
void door_go_down(edict_t *self);
|
|
|
|
/*
|
|
* 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
|
|
door_use(edict_t *self, edict_t *other /* unused */, edict_t *activator)
|
|
{
|
|
if (!self || !activator)
|
|
{
|
|
return;
|
|
}
|
|
|
|
edict_t *ent;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/* 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)
|
|
{
|
|
/* 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;
|
|
}
|
|
|
|
/* Map quirk for waste3 (to make that secret armor behind
|
|
* the secret wall - this func_door - count, #182) */
|
|
if (Q_stricmp(level.mapname, "waste3") == 0 && Q_stricmp(ent->model, "*12") == 0)
|
|
{
|
|
ent->target = "t117";
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* 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 */
|
|
self->moveinfo.sound_start = gi.soundindex("world/mov_watr.wav");
|
|
self->moveinfo.sound_end = gi.soundindex("world/stp_watr.wav");
|
|
break;
|
|
|
|
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->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);
|
|
}
|
|
|
|
/* ==================================================================== */
|
|
|
|
#define TRAIN_START_ON 1
|
|
#define TRAIN_TOGGLE 2
|
|
#define TRAIN_BLOCK_STOPS 4
|
|
|
|
void train_next(edict_t *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)
|
|
{
|
|
/* 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)
|
|
{
|
|
train_next(self);
|
|
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_next(edict_t *self)
|
|
{
|
|
if (!self)
|
|
{
|
|
return;
|
|
}
|
|
|
|
edict_t *ent;
|
|
vec3_t dest;
|
|
qboolean first;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 || !activator)
|
|
{
|
|
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)
|
|
*/
|
|
|
|
#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
|
|
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)
|
|
{
|
|
/* 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)
|
|
{
|
|
vec3_t forward, right, up;
|
|
float side;
|
|
float width;
|
|
float length;
|
|
|
|
if (!ent)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|