866 lines
30 KiB
Plaintext
866 lines
30 KiB
Plaintext
/*======================================================================
|
|
SUB FUNCTIONS
|
|
======================================================================*/
|
|
void() SUB_Null = {};
|
|
void() SUB_Remove = {remove(self);};
|
|
void(entity inflictor, entity attacker, float damage) SUB_Null_pain = {};
|
|
|
|
void() SUB_CalcMoveDone;
|
|
void() SUB_CalcAngleMoveDone;
|
|
void(vector marker_org, float marker_type) spawn_marker;
|
|
void(float hud_item) update_hud_totals;
|
|
|
|
//======================================================================
|
|
// To determine if an entity is still active this function generates
|
|
// an unique number that can be checked/shared between linked entities
|
|
//----------------------------------------------------------------------
|
|
void(entity targ) gen_unique_no =
|
|
{
|
|
// Check if target entity has an unique number already
|
|
if (targ.entno_unique == 0)
|
|
targ.entno_unique = rint(random()*1000) * rint(random()*1000);
|
|
};
|
|
|
|
//======================================================================
|
|
// Most of the monsters have an attack_offset setup to spawn their
|
|
// proejctiles in the right location relative to their gun model
|
|
// This function expects the makevectors has been done already
|
|
// makevectors (self.angles); <--- if missing, vector will be rubbish!
|
|
//----------------------------------------------------------------------
|
|
vector(vector attack_dir) attack_vector =
|
|
{
|
|
local vector retvec;
|
|
retvec = v_forward * attack_dir_x;
|
|
retvec = retvec + (v_right * attack_dir_y);
|
|
retvec = retvec + (v_up * attack_dir_z);
|
|
return retvec;
|
|
};
|
|
|
|
//======================================================================
|
|
// Bmodels have a 0,0,0 origin key, need to calculate from mins/maxs
|
|
// This function will return the true origin using mins/max vectors
|
|
//----------------------------------------------------------------------
|
|
vector(entity bmodel_ent) bmodel_origin =
|
|
{
|
|
local vector vdiff, vorigin;
|
|
|
|
// Calculate the middle point of the mins/maxs size vectors
|
|
vdiff_x = (bmodel_ent.maxs_x - bmodel_ent.mins_x) / 2;
|
|
vdiff_y = (bmodel_ent.maxs_y - bmodel_ent.mins_y) / 2;
|
|
vdiff_z = (bmodel_ent.maxs_z - bmodel_ent.mins_z) / 2;
|
|
|
|
// The mins vector is an absolute value of where the Bmodel is located
|
|
// Add mins vector to middle point to get true origin
|
|
// When bmodel do move they also update their origin co-ordinates
|
|
// So any additional movement needs to be added as well
|
|
// This means that bmodels can be tracked by movement functions
|
|
vorigin = bmodel_ent.origin + bmodel_ent.mins + vdiff;
|
|
|
|
// Return origin
|
|
return vorigin;
|
|
};
|
|
|
|
//======================================================================
|
|
// Check for bmodel errors, model key empty (can crash engine)
|
|
//----------------------------------------------------------------------
|
|
float() check_bmodel_keys =
|
|
{
|
|
if (self.model == "") {
|
|
// this error can be fatal for most engines, show warning
|
|
dprint ("\b[BMODEL]\b "); dprint (self.classname);
|
|
dprint (" ("); dprint (self.targetname);
|
|
dprint (") missing MODEL key!\n");
|
|
|
|
self.classtype = CT_DEVMARKER; // Useless entity
|
|
self.movetype = MOVETYPE_NONE; // Stationary
|
|
self.solid = SOLID_NOT; // no world interaction
|
|
|
|
// If developer active show yellow diamond
|
|
if (developer > 0) setmodel(self, MODEL_BROKEN);
|
|
else setmodel(self,"");
|
|
|
|
// Make sure no world interaction
|
|
setsize (self, VEC_ORIGIN, VEC_ORIGIN);
|
|
|
|
// Randomly place debug diamond above 0,0,0
|
|
self.pos1 = '0 0 0';
|
|
self.pos1_z = random()*64;
|
|
setorigin(self, self.pos1);
|
|
|
|
// Do nothing with this entity
|
|
return TRUE;
|
|
}
|
|
else return FALSE;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Cannot have multiple upgrade restrictions on monster/items
|
|
//----------------------------------------------------------------------
|
|
void() remove_duplicate_upgrades =
|
|
{
|
|
if (self.upgrade_axe) {
|
|
if (self.upgrade_ssg) self.upgrade_ssg = FALSE;
|
|
else if (self.upgrade_lg) self.upgrade_lg = FALSE;
|
|
}
|
|
else if (self.upgrade_ssg) {
|
|
if (self.upgrade_axe) self.upgrade_axe = FALSE;
|
|
else if (self.upgrade_lg) self.upgrade_lg = FALSE;
|
|
}
|
|
else if (self.upgrade_lg) {
|
|
if (self.upgrade_axe) self.upgrade_axe = FALSE;
|
|
else if (self.upgrade_ssg) self.upgrade_ssg = FALSE;
|
|
}
|
|
}
|
|
|
|
/*======================================================================
|
|
QuakeEd only writes a single float for angles (bad idea),
|
|
so up and down are just constant angles.
|
|
======================================================================*/
|
|
void() SetMovedir =
|
|
{
|
|
if (self.angles == '0 -1 0') self.movedir = '0 0 1';
|
|
else if (self.angles == '0 -2 0') self.movedir = '0 0 -1';
|
|
else {
|
|
makevectors (self.angles);
|
|
self.movedir = v_forward;
|
|
}
|
|
// Bmodels need angles key reset otherwise twisted on spawn
|
|
self.angles = '0 0 0';
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
float(vector check_vec) CheckZeroVector =
|
|
{
|
|
if (check_vec_x == 0 && check_vec_y == 0 && check_vec_z == 0)
|
|
return TRUE;
|
|
else return FALSE;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Some entities have directional parameters (angles -> movedir)
|
|
// This function searches for a target entity (useful for tracking)
|
|
// Always finds the first target key (will fail if multiple targets)
|
|
//----------------------------------------------------------------------
|
|
void() TargetMovedir =
|
|
{
|
|
local entity targ;
|
|
local vector org, targorg;
|
|
|
|
if (self.target == "") return;
|
|
|
|
// Check self for Bmodel origin
|
|
if (self.bsporigin) org = bmodel_origin(self);
|
|
else org = self.origin;
|
|
|
|
// Find target entity
|
|
targ = find(world, targetname, self.target);
|
|
if (targ) {
|
|
// Check for a Bmodel object (special origin)
|
|
if (targ.bsporigin) targorg = bmodel_origin(targ);
|
|
else targorg = targ.origin;
|
|
// Calculate facing angle towards target
|
|
self.movedir = normalize(targorg - org);
|
|
}
|
|
};
|
|
|
|
/*======================================================================
|
|
InitTrigger
|
|
======================================================================*/
|
|
void() InitTrigger =
|
|
{
|
|
// trigger angles are used for one-way touches. An angle of 0 is assumed
|
|
// to mean no restrictions, so use a yaw of 360 instead.
|
|
if (CheckZeroVector(self.angles) == FALSE) SetMovedir ();
|
|
self.solid = SOLID_TRIGGER;
|
|
self.movetype = MOVETYPE_NONE;
|
|
self.mdl = self.model; // Save model for later
|
|
setmodel (self, self.mdl); // set size and link into world
|
|
self.bbmins = self.mins; // Save bmodel bounds for later
|
|
self.bbmaxs = self.maxs;
|
|
self.bsporigin = TRUE; // bmodel origin 0,0,0
|
|
self.modelindex = 0;
|
|
self.model = ""; // hide bmodel surface
|
|
};
|
|
|
|
/*======================================================================
|
|
SUB_CalcMove
|
|
|
|
calculate self.velocity and self.nextthink to reach dest from
|
|
self.origin traveling at speed
|
|
======================================================================*/
|
|
void(vector tdest, float tspeed, void() func) SUB_CalcMove =
|
|
{
|
|
local vector vdestdelta;
|
|
local float len, traveltime;
|
|
|
|
if (!tspeed) tspeed = 40; // Create default speed if none specified
|
|
self.think1 = func; // Finished moving function
|
|
self.finaldest = tdest; // Final destination
|
|
self.think = SUB_CalcMoveDone; // Tidy up function
|
|
|
|
vdestdelta = tdest - self.origin; // set destdelta to the vector needed to move
|
|
len = vlen (vdestdelta); // calculate length of vector
|
|
traveltime = len / tspeed; // divide by speed to get time to reach dest
|
|
|
|
// If move distance or time too small, snap to final location
|
|
if (traveltime < 0.1 || tdest == self.origin) {
|
|
self.velocity = '0 0 0';
|
|
self.nextthink = self.ltime + 0.1;
|
|
return;
|
|
}
|
|
|
|
// scale the destdelta vector by the time spent traveling to get velocity
|
|
self.velocity = vdestdelta * (1/traveltime); // qcc won't take vec/float
|
|
self.nextthink = self.ltime + traveltime;
|
|
};
|
|
|
|
/*======================================================================
|
|
After moving, set origin to exact final destination
|
|
======================================================================*/
|
|
void() SUB_CalcMoveDone =
|
|
{
|
|
self.velocity = '0 0 0';
|
|
setorigin(self, self.finaldest);
|
|
self.nextthink = -1;
|
|
if (self.think1) self.think1();
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Not used anywhere
|
|
//----------------------------------------------------------------------
|
|
void(entity ent, vector tdest, float tspeed, void() func) SUB_CalcMoveEnt =
|
|
{
|
|
local entity stemp;
|
|
stemp = self;
|
|
self = ent;
|
|
|
|
SUB_CalcMove (tdest, tspeed, func);
|
|
self = stemp;
|
|
};
|
|
|
|
/*======================================================================
|
|
SUB_CalcMoveEntity
|
|
|
|
calculate self.velocity and self.nextthink to reach dest from
|
|
self.origin traveling at speed
|
|
======================================================================*/
|
|
void() SUB_MoveEntityDone =
|
|
{
|
|
self.movetype = self.classmove;
|
|
SUB_CalcMoveDone();
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void(vector tdest, float tspeed, void() func) SUB_CalcMoveEntity =
|
|
{
|
|
local vector vdestdelta;
|
|
local float traveltime;
|
|
|
|
if (!tspeed) tspeed = 4; // Create default travel time if none specified
|
|
self.classmove = self.movetype; // save previous movetype
|
|
self.movetype = MOVETYPE_FLY; // Allow entity to move smoothly
|
|
self.think1 = func; // Store final function
|
|
self.finaldest = tdest; // Store final destination
|
|
|
|
vdestdelta = tdest - self.origin; // set destdelta to the vector needed to move
|
|
traveltime = 1 / tspeed; // Work out percentage travel velocity
|
|
self.velocity = vdestdelta * traveltime; // Calc time travel fraction
|
|
|
|
self.nextthink = time + tspeed;
|
|
self.think = SUB_MoveEntityDone;
|
|
};
|
|
|
|
/*======================================================================
|
|
SUB_CalcAngleMove
|
|
|
|
calculate self.avelocity and self.nextthink to reach destangle from
|
|
self.angles rotating
|
|
|
|
The calling function should make sure self.think is valid
|
|
======================================================================*/
|
|
void(vector destangle, float tspeed, void() func) SUB_CalcAngleMove =
|
|
{
|
|
local vector destdelta;
|
|
local float len, traveltime;
|
|
|
|
if (!tspeed) dprint("[SUB_CALC_ANGLE] No speed is defined!");
|
|
|
|
// set destdelta to the vector needed to move
|
|
destdelta = destangle - self.angles;
|
|
|
|
// calculate length of vector
|
|
len = vlen (destdelta);
|
|
|
|
// divide by speed to get time to reach dest
|
|
traveltime = len / tspeed;
|
|
|
|
// set nextthink to trigger a think when dest is reached
|
|
self.nextthink = self.ltime + traveltime;
|
|
|
|
// scale the destdelta vector by the time spent traveling to get velocity
|
|
self.avelocity = destdelta * (1 / traveltime);
|
|
|
|
self.think1 = func;
|
|
self.finalangle = destangle;
|
|
self.think = SUB_CalcAngleMoveDone;
|
|
};
|
|
|
|
/*======================================================================
|
|
After rotating, set angle to exact final angle
|
|
======================================================================*/
|
|
void() SUB_CalcAngleMoveDone =
|
|
{
|
|
self.angles = self.finalangle;
|
|
self.avelocity = '0 0 0';
|
|
self.nextthink = -1;
|
|
if (self.think1)
|
|
self.think1();
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Not used anywhere
|
|
//----------------------------------------------------------------------
|
|
void(entity ent, vector destangle, float tspeed, void() func) SUB_CalcAngleMoveEnt =
|
|
{
|
|
local entity stemp;
|
|
stemp = self;
|
|
self = ent;
|
|
SUB_CalcAngleMove (destangle, tspeed, func);
|
|
self = stemp;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() DelayThink =
|
|
{
|
|
activator = self.enemy;
|
|
SUB_UseTargets ();
|
|
remove(self);
|
|
};
|
|
|
|
/*======================================================================
|
|
SUB_UseTargets
|
|
|
|
the global "activator" should be set to the entity that initiated the firing.
|
|
|
|
If self.delay is set, a DelayedUse entity will be created that will actually
|
|
do the SUB_UseTargets after that many seconds have passed.
|
|
|
|
Centerprints any self.message to the activator.
|
|
|
|
Removes all entities with a targetname that match self.killtarget,
|
|
and removes them, so some events can remove other triggers.
|
|
|
|
Search for (string)targetname in all entities that
|
|
match (string)self.target and call their .use function
|
|
|
|
======================================================================*/
|
|
void() SUB_UseTargets =
|
|
{
|
|
local entity t, stemp, otemp, act;
|
|
|
|
//----------------------------------------------------------------------
|
|
// Check for recursive loops!?!
|
|
//----------------------------------------------------------------------
|
|
if (self.targetname != "") {
|
|
if (self.targetname == self.target || self.targetname == self.target2) {
|
|
dprint("\n----------------------------------------\n");
|
|
dprint("\b[USE_TARGET]\b target == targetname, really bad!!!\n");
|
|
dprint(self.classname);
|
|
dprint(" ("); dprint(self.targetname); dprint(") at ");
|
|
if (self.bsporigin) dprint(vtos(bmodel_origin(self)));
|
|
else dprint(vtos(self.origin));
|
|
dprint("\ntarg ("); dprint(self.target);
|
|
dprint(") targ2 ("); dprint(self.target2);
|
|
dprint(") kill ("); dprint(self.killtarget);
|
|
dprint(")\n----------------------------------------\n");
|
|
return;
|
|
}
|
|
}
|
|
//----------------------------------------------------------------------
|
|
// check for a delay
|
|
//----------------------------------------------------------------------
|
|
if (self.delay) {
|
|
// create a temp object to fire at a later time
|
|
t = spawn();
|
|
t.classname = "DelayedUse";
|
|
t.classtype = CT_TRIGRELAYDELAY;
|
|
t.owner = self;
|
|
t.nextthink = time + self.delay;
|
|
t.think = DelayThink;
|
|
t.enemy = activator;
|
|
t.message = self.message;
|
|
t.killtarget = self.killtarget;
|
|
t.target = self.target;
|
|
t.target2 = self.target2;
|
|
t.noise = self.noise;
|
|
return;
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Check if previous delay has been disabled
|
|
// Unfortunately start off = OFF state and not easy to check for
|
|
// because some entities use that spawnflag bit for other things!
|
|
//----------------------------------------------------------------------
|
|
if (self.classtype == CT_TRIGRELAYDELAY) {
|
|
if (self.owner.estate & ESTATE_DISABLE) return;
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// print any messages
|
|
//----------------------------------------------------------------------
|
|
if (activator.flags & FL_CLIENT && self.message != "") {
|
|
if (coop > 0) {
|
|
// Write message to all clients
|
|
msg_entity = self;
|
|
WriteByte (MSG_ALL, SVC_CENTERPRINT);
|
|
WriteString (MSG_ALL, self.message);
|
|
}
|
|
// Write message to single client
|
|
else centerprint (activator, self.message);
|
|
// Default noise = classic beep beep sound
|
|
if (self.noise == "") sound (activator, CHAN_VOICE, SOUND_TALK, 1, ATTN_NORM);
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// kill targets
|
|
// I really do hate this function because there is hardly any checks
|
|
// on entities being removed, added a player check for sanity!
|
|
//----------------------------------------------------------------------
|
|
if (self.killtarget != "") {
|
|
t = find (world, targetname, self.killtarget);
|
|
while(t) {
|
|
// Check for activate mosnters? Tidy up kill counter
|
|
if (t.flags & FL_MONSTER && t.health > 0) {
|
|
// If a monster marked with no monster count dies
|
|
// update HUD totals to reflect death properly
|
|
if (t.nomonstercount) {
|
|
total_monsters = total_monsters + 1;
|
|
update_hud_totals(HUD_MONSTERS);
|
|
}
|
|
// Update global monster death totals
|
|
killed_monsters = killed_monsters + 1;
|
|
WriteByte (MSG_ALL, SVC_KILLEDMONSTER);
|
|
}
|
|
// Added extra check to prevent player client removal!?!
|
|
if (!(t.flags & FL_CLIENT)) {
|
|
// Try to minimize the entity chain damage, check for
|
|
// entity attachments and delete them as well!
|
|
if (t.attachment) remove(t.attachment);
|
|
if (t.attachment2) remove(t.attachment2);
|
|
if (t.attachment3) remove(t.attachment3);
|
|
// Make sure any unique ent number is reset
|
|
if (t.entno_unique > 0) t.entno_unique = 0;
|
|
// Remove entity from chain list
|
|
remove (t);
|
|
}
|
|
// Find the next killtarget
|
|
t = find (t, targetname, self.killtarget);
|
|
}
|
|
}
|
|
//----------------------------------------------------------------------
|
|
// fire targets
|
|
//----------------------------------------------------------------------
|
|
if (self.target != "") {
|
|
act = activator; // ID Code - Never switch around, not sure why it is here?
|
|
t = find (world, targetname, self.target);
|
|
stemp = self; otemp = other; // Store self/other
|
|
while(t) {
|
|
if (t.use != SUB_Null) {
|
|
if (t.use) {
|
|
self = t; other = stemp; // Switch self/other
|
|
self.activate = stemp; // used by the breakable system
|
|
self.use (); // fire trigger
|
|
self = stemp; other = otemp; // restore self/other
|
|
activator = act; // ID code - Never changed, why restore it?
|
|
}
|
|
}
|
|
// Are there anymore targets left in the list?
|
|
t = find (t, targetname, self.target);
|
|
}
|
|
}
|
|
//----------------------------------------------------------------------
|
|
if (self.target2 != "") {
|
|
act = activator; // ID Code - Never switch around, not sure why it is here?
|
|
t = find (world, targetname, self.target2);
|
|
stemp = self; otemp = other; // Store self/other
|
|
while(t) {
|
|
if (t.use != SUB_Null) {
|
|
if (t.use) {
|
|
self = t; other = stemp;// Switch self/other
|
|
self.activate = stemp; // used mostly by the breakable system
|
|
self.use (); // fire trigger
|
|
self = stemp; other = otemp; // restore self/other
|
|
activator = act; // ID code - Never changed, why restore it?
|
|
}
|
|
}
|
|
t = find (t, targetname, self.target2);
|
|
}
|
|
}
|
|
};
|
|
|
|
/*======================================================================
|
|
SUB_AttackFinished
|
|
in nightmare mode, all attack_finished times become 0
|
|
some monsters refire twice automatically (using .cnt variable)
|
|
Change nightmare to be less range spam and better time scaling
|
|
======================================================================*/
|
|
void(float normal) SUB_AttackFinished =
|
|
{
|
|
local float nightadj; // Easier to work out percentage
|
|
self.cnt = 0; // reset checkfire counter
|
|
|
|
// Nightmare skill attacks are faster (turret like) and with
|
|
// hitscan enemies this can easily overwhelm targets locking
|
|
// them into damage/pain function loops. This can especially be
|
|
// be bad if a large enough enemy group is involved!
|
|
// Changed - NM behaviour for boss fights is default
|
|
//
|
|
if (skill == SKILL_NIGHTMARE && self.enemy.bossflag == FALSE) {
|
|
// Nightmare mode is just endless range attacks!
|
|
// I never thought this was a good way to end skill levels
|
|
// Changed nightmare mode to 50-75% decrease in time
|
|
// Exceptions to nightmare mode (minion spawning) - 75-100% time
|
|
nightadj = 0.5 + (random()*0.25);
|
|
if (self.minion_active) nightadj = nightadj + 0.25;
|
|
// The original ID shamblers were lightning turrets on Nightmare
|
|
// Shamblers don't scale with skill levels in AD because
|
|
// they don't have projectiles, so they need a switch instead
|
|
// On Nightmare skill Shamblers go from 2-4s to 1s!
|
|
// Instead of 2-4s, NM is 0.6-1.2s, Average 1.2s +2 frames
|
|
if (self.classtype == CT_MONSHAM) nightadj = 0.4;
|
|
self.attack_finished = time + (normal*nightadj);
|
|
}
|
|
// Easy, Nornal and Hard skill all pass through, no change
|
|
else self.attack_finished = time + normal;
|
|
//dprint("Time ("); dprint(ftos(time));
|
|
//dprint(") AF ("); dprint(ftos(normal));
|
|
//dprint(") Dif ("); dprint(ftos(self.attack_finished - time));
|
|
//dprint(") Att ("); dprint(ftos(self.attack_finished));
|
|
//dprint(")\n");
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Used by soliders and enforcers (special double fire mode)
|
|
//----------------------------------------------------------------------
|
|
void (void() thinkst) SUB_CheckRefire =
|
|
{
|
|
if (skill != SKILL_NIGHTMARE) return;
|
|
if (self.cnt == 1) return;
|
|
if (!visible (self.enemy)) return;
|
|
self.cnt = 1;
|
|
self.think = thinkst;
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
// Not used anywhere in QC
|
|
//----------------------------------------------------------------------
|
|
/*float() SUB_CountTargets =
|
|
{
|
|
local entity t;
|
|
local float i;
|
|
|
|
i = 0;
|
|
if (self.target != "") {
|
|
t = world;
|
|
do {
|
|
t = find(t, targetname, self.target);
|
|
if (!t) break;
|
|
i = i + 1;
|
|
} while ( 1 );
|
|
}
|
|
return i;
|
|
};*/
|
|
|
|
/*======================================================================
|
|
ChooseTurn
|
|
- can't find any reference to this in the QC
|
|
======================================================================*/
|
|
/*void(vector dest3) ChooseTurn =
|
|
{
|
|
local vector dir, newdir;
|
|
|
|
dir = self.origin - dest3;
|
|
|
|
newdir_x = trace_plane_normal_y;
|
|
newdir_y = 0 - trace_plane_normal_x;
|
|
newdir_z = 0;
|
|
|
|
if (dir * newdir > 0) {
|
|
dir_x = 0 - trace_plane_normal_y;
|
|
dir_y = trace_plane_normal_x;
|
|
}
|
|
else {
|
|
dir_x = trace_plane_normal_y;
|
|
dir_y = 0 - trace_plane_normal_x;
|
|
}
|
|
|
|
dir_z = 0;
|
|
self.ideal_yaw = vectoyaw(dir);
|
|
};*/
|
|
|
|
/*======================================================================
|
|
SPAWN_TFOG and SPAWN_TDEATH
|
|
|
|
These functions were originally in triggers.qc, but are used all
|
|
over the place by monsters, items and players. Subs seems a better
|
|
home for these generic functions.
|
|
|
|
======================================================================*/
|
|
void() play_teleport =
|
|
{
|
|
self.lip = random() * 5;
|
|
if (self.lip < 1) self.noise = "misc/r_tele1.wav";
|
|
else if (self.lip < 2) self.noise = "misc/r_tele2.wav";
|
|
else if (self.lip < 3) self.noise = "misc/r_tele3.wav";
|
|
else if (self.lip < 4) self.noise = "misc/r_tele4.wav";
|
|
else self.noise = "misc/r_tele5.wav";
|
|
|
|
sound (self, CHAN_VOICE, self.noise, 1, ATTN_NORM);
|
|
self.nextthink = time + 2; // Allow time for the sound to finish
|
|
self.think = SUB_Remove; // tidy up function
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void(vector org) spawn_tfog =
|
|
{
|
|
local entity spawneffect;
|
|
|
|
spawneffect = spawn ();
|
|
spawneffect.origin = org;
|
|
spawneffect.nextthink = time + 0.2;
|
|
spawneffect.think = play_teleport;
|
|
|
|
WriteByte (MSG_BROADCAST, SVC_TEMPENTITY);
|
|
WriteByte (MSG_BROADCAST, TE_TELEPORT);
|
|
WriteCoord (MSG_BROADCAST, org_x);
|
|
WriteCoord (MSG_BROADCAST, org_y);
|
|
WriteCoord (MSG_BROADCAST, org_z);
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void() tdeath_touch =
|
|
{
|
|
// Cannot telefrag yourself!?!
|
|
if (other == self.owner) return;
|
|
// Check if found a breakable than cannot be broken
|
|
if (ai_foundbreakable(self, other, FALSE)) return;
|
|
|
|
// A boss cannot be telefragged (except shub)
|
|
// Telefrag entity trying to telefrag boss instead!?!
|
|
if (other.bossflag == TRUE) {
|
|
if (other.classtype != CT_MONIDSHUB) {
|
|
T_Damage (self.owner, other, other, MEGADEATH, NOARMOR);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// frag anyone who teleports in on top of an invincible player
|
|
if (other.flags & FL_CLIENT) {
|
|
if (other.invincible_finished > time) self.classname = "teledeath2";
|
|
// other monsters explode themselves
|
|
if ( !(self.owner.flags & FL_CLIENT) ) {
|
|
T_Damage (self.owner, self, self, MEGADEATH, NOARMOR);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Sudden death incoming ...
|
|
if (other.health) T_Damage (other, self, self, MEGADEATH, NOARMOR);
|
|
};
|
|
|
|
//----------------------------------------------------------------------
|
|
void(vector org, entity death_owner) spawn_tdeath =
|
|
{
|
|
local entity death;
|
|
|
|
death = spawn();
|
|
death.classname = "teledeath";
|
|
death.owner = death_owner;
|
|
death.movetype = MOVETYPE_NONE;
|
|
death.solid = SOLID_TRIGGER;
|
|
death.angles = '0 0 0';
|
|
setsize (death, death_owner.mins - '1 1 1', death_owner.maxs + '1 1 1');
|
|
setorigin (death, org);
|
|
death.touch = tdeath_touch;
|
|
death.nextthink = time + 0.2;
|
|
death.think = SUB_Remove;
|
|
|
|
// This is extremely costly on the engine because it re-checks
|
|
// every trigger in the map to see if it is touching something
|
|
force_retouch = 2;
|
|
};
|
|
|
|
/*======================================================================
|
|
skill_chanceattack
|
|
Calculate chance to attack player based on skill level and distance
|
|
The standard ID range formula
|
|
(enemy_range == RANGE_MELEE) chance = 0.9
|
|
(enemy_range == RANGE_NEAR) chance = 0.4
|
|
(enemy_range == RANGE_MID) chance = 0.05
|
|
else chance = 0;
|
|
The problem with this formula is that the chance factor is not gradual
|
|
and will spike in chance when close to range boundaries
|
|
Also this formula has no skill modifiers leading to limited gameplay rules
|
|
|
|
======================================================================*/
|
|
float() SUB_ChanceAttack =
|
|
{
|
|
local float chrange, chskill, chance;
|
|
|
|
chrange = chskill = 0; // taniwha patch
|
|
|
|
// Upper and lower limits of range have fixed chance rates
|
|
if (self.enemydist > MON_MAX_RANGE) chance = 0.05;
|
|
else if (self.enemydist < MON_RANGE_MELEE) chance = 0.95;
|
|
else {
|
|
// Random chance of range attack based on player distance
|
|
chrange = ((MON_MAX_RANGE - self.enemydist) / MON_STEPRANGE) / 10;
|
|
|
|
// Skill modifier to chance (Easy -0.2 Norm -0.1, Hard 0, Night 0.1)
|
|
chskill = ((skill-SKILL_HARD)*0.1);
|
|
chance = chrange + chskill;
|
|
|
|
// There Should always be a chance to HIT or MISS
|
|
if (chance > 0.95) chance = 0.95;
|
|
else if (chance < 0.05) chance = 0.05;
|
|
}
|
|
|
|
// Return result and finish debug information line
|
|
if (random() < chance) return TRUE;
|
|
else return FALSE;
|
|
};
|
|
|
|
/*======================================================================
|
|
skill_subattack
|
|
Calculate attack_finished time based on skill level
|
|
======================================================================*/
|
|
float (float min_time, float ranbase_time) SUB_SkillAttack =
|
|
{
|
|
local float ran_time, sub_time;
|
|
|
|
// Easy = min + random(base + 1.5) Normal = min + random(base + 1.0)
|
|
// Hard = min + random(base + 0.5) Nightm = min + random(base + 0.0)
|
|
ran_time = ranbase_time + ((SKILL_NIGHTMARE - skill) * 0.5);
|
|
sub_time = min_time + (ran_time * random());
|
|
|
|
return sub_time;
|
|
};
|
|
|
|
/*======================================================================
|
|
SUB_Tracking
|
|
Progressively tracks the origin of enemies making attacks more accurate
|
|
* Original code by Necros (http://shoresofnis.com/)
|
|
======================================================================*/
|
|
vector(vector iorg, vector org_offset, entity targ, float zspeed) SUB_Tracking =
|
|
{
|
|
local vector vec, org;
|
|
local float diff;
|
|
|
|
// Allow for source origin to be fine tuned (directional)
|
|
makevectors (self.angles);
|
|
org = self.origin + attack_vector(org_offset);
|
|
|
|
diff = vlen(iorg - org) / zspeed; // Workout difference
|
|
vec = targ.velocity;
|
|
if (targ.flags & FL_MONSTER) vec_z = 0; // Flatten Z axis for monsters
|
|
else vec_z = vec_z * 0.2; // Aim for headshot with players
|
|
|
|
iorg = targ.origin + diff * vec; // Refine target origin
|
|
return iorg;
|
|
};
|
|
|
|
/*======================================================================
|
|
SUB_Elevation
|
|
Calculates the Elevation for Z Aware monsters
|
|
* Original code by Preach (https://tomeofpreach.wordpress.com/)
|
|
* Added support for variable origin locations and proj speeds
|
|
* Added support for distance scaling linked to skill levels
|
|
======================================================================*/
|
|
float(float theta, vector sorg, vector mdest, float zspeed) SUB_Elevation =
|
|
{
|
|
local float a, b, c; //constants in the equation to be solved
|
|
local vector ofs; //displacement we wish the projectile to travel
|
|
local float y, z; //horizontal and vertical components of ofs
|
|
local float tan_theta; //trig values of the angle theta
|
|
local vector vec, eorg;
|
|
local float dist, sperc;
|
|
|
|
// Place projectile destination at player head level
|
|
if (self.classtype == CT_MONSPIDER) mdest = mdest + ELEV_SPID_OFS;
|
|
else mdest = mdest + ELEV_VIEW_OFS;
|
|
|
|
// Work out vector distance between source and destination
|
|
// Normalize the distance and create a percentage of that
|
|
// This is a skill based calculation so the aiming gets better
|
|
// The grenade is aimed infront of target so that it bounces
|
|
// No check for vertical difference so it bad going up height!
|
|
// Zombie, poison spiders zaware calculation is fine, exception!
|
|
if (self.classgroup != CG_ZOMBIE && self.classtype != CT_MONSPIDER
|
|
&& self.classtype != CT_MONBOGLORD) {
|
|
vec = mdest - sorg;
|
|
dist = vlen(vec);
|
|
vec = normalize(vec);
|
|
sperc = 0.45 + skill*0.15;
|
|
eorg = sorg + (vec * (dist*sperc));
|
|
ofs = eorg - sorg;
|
|
}
|
|
else ofs = mdest - sorg;
|
|
|
|
//calculate how far we are firing
|
|
// ofs = mdest - sorg;
|
|
z = ofs_z;
|
|
ofs_z = 0;
|
|
y = vlen(ofs);
|
|
|
|
// Map gravity is a global variable defined in world.qc
|
|
//find the coefficients of the quadratic in tan(theta)
|
|
a = 0.5 * map_gravity * y * y / (zspeed * zspeed);
|
|
b = -y;
|
|
c = a + z;
|
|
|
|
//check if the destination is too far to reach
|
|
if(b*b < 4*a*c) return ELEV_DEFAULT;
|
|
|
|
//calculate the tan value of the given theta
|
|
tan_theta = mathlib_tan(theta);
|
|
|
|
//reuse ang to create the improved firing direction
|
|
theta = mathlib_atan2(a*tan_theta*tan_theta - c, 2*a*tan_theta + b);
|
|
|
|
//constrain the values to stop anything too mad happening
|
|
while(theta > 90) theta = theta - 180;
|
|
return theta;
|
|
};
|
|
|
|
/*======================================================================
|
|
SUB_Elevat (Alternative version, not used anymore, left for reference)
|
|
Calculates the Elevation for Z Aware monsters
|
|
* Original formula WIKI with QC help from ericw
|
|
======================================================================*/
|
|
/*float(vector source, vector targ, float zspeed) SUB_Elevat =
|
|
{
|
|
local float g; // Gravity
|
|
local float targx, targy; // Sub parts of distance
|
|
local float v2, v4; // speed*speed & speed*speed*speed*speed
|
|
local vector dist; // Distance to travel
|
|
local float ang, theta;
|
|
|
|
g = map_gravity; // default 800
|
|
dist = targ - source; // vector distance
|
|
targy = dist_z; // Up direction
|
|
dist_z = 0;
|
|
targx = vlen(dist); // X,Y direction
|
|
|
|
v2 = zspeed * zspeed;
|
|
v4 = v2 * v2;
|
|
|
|
// First part of equation
|
|
ang = v4 - (g * ( (g * targx * targx) + (2 * targy * v2) ) );
|
|
// Cap elevation angle at default value
|
|
//if(ang < 0) return ELEV_DEFAULT;
|
|
|
|
// Second section of equation
|
|
theta = mathlib_atan2 ((v2 - mathlib_sqrt(ang)) / (g * targx),1);
|
|
return theta;
|
|
}; */
|