forked from OctaForge/OctaCore
785 lines
26 KiB
C++
785 lines
26 KiB
C++
// weapon.cpp: all shooting and effects code, projectile management
|
|
#include "game.h"
|
|
|
|
namespace game
|
|
{
|
|
static const int OFFSETMILLIS = 500;
|
|
vec rays[MAXRAYS];
|
|
|
|
struct hitmsg
|
|
{
|
|
int target, lifesequence, info1, info2;
|
|
ivec dir;
|
|
};
|
|
vector<hitmsg> hits;
|
|
|
|
#if 0
|
|
#define MINDEBRIS 3
|
|
VARP(maxdebris, MINDEBRIS, 10, 100);
|
|
VARP(maxgibs, 0, 4, 100);
|
|
#endif
|
|
|
|
ICOMMAND(getweapon, "", (), intret(player1->gunselect));
|
|
|
|
void gunselect(int gun, gameent *d)
|
|
{
|
|
if(gun!=d->gunselect)
|
|
{
|
|
addmsg(N_GUNSELECT, "rci", d, gun);
|
|
playsound(S_WEAPLOAD, d == player1 ? NULL : &d->o);
|
|
}
|
|
d->gunselect = gun;
|
|
}
|
|
|
|
void nextweapon(int dir, bool force = false)
|
|
{
|
|
if(player1->state!=CS_ALIVE) return;
|
|
dir = (dir < 0 ? NUMGUNS-1 : 1);
|
|
int gun = player1->gunselect;
|
|
loopi(NUMGUNS)
|
|
{
|
|
gun = (gun + dir)%NUMGUNS;
|
|
if(force || player1->ammo[gun]) break;
|
|
}
|
|
if(gun != player1->gunselect) gunselect(gun, player1);
|
|
else playsound(S_NOAMMO);
|
|
}
|
|
ICOMMAND(nextweapon, "ii", (int *dir, int *force), nextweapon(*dir, *force!=0));
|
|
|
|
int getweapon(const char *name)
|
|
{
|
|
if(isdigit(name[0])) return parseint(name);
|
|
else
|
|
{
|
|
int len = strlen(name);
|
|
loopi(sizeof(guns)/sizeof(guns[0])) if(!strncasecmp(guns[i].name, name, len)) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void setweapon(const char *name, bool force = false)
|
|
{
|
|
int gun = getweapon(name);
|
|
if(player1->state!=CS_ALIVE || !validgun(gun)) return;
|
|
if(force || player1->ammo[gun]) gunselect(gun, player1);
|
|
else playsound(S_NOAMMO);
|
|
}
|
|
ICOMMAND(setweapon, "si", (char *name, int *force), setweapon(name, *force!=0));
|
|
|
|
void cycleweapon(int numguns, int *guns, bool force = false)
|
|
{
|
|
if(numguns<=0 || player1->state!=CS_ALIVE) return;
|
|
int offset = 0;
|
|
loopi(numguns) if(guns[i] == player1->gunselect) { offset = i+1; break; }
|
|
loopi(numguns)
|
|
{
|
|
int gun = guns[(i+offset)%numguns];
|
|
if(gun>=0 && gun<NUMGUNS && (force || player1->ammo[gun]))
|
|
{
|
|
gunselect(gun, player1);
|
|
return;
|
|
}
|
|
}
|
|
playsound(S_NOAMMO);
|
|
}
|
|
ICOMMAND(cycleweapon, "V", (tagval *args, int numargs),
|
|
{
|
|
int numguns = min(numargs, 3);
|
|
int guns[3];
|
|
loopi(numguns) guns[i] = getweapon(args[i].getstr());
|
|
cycleweapon(numguns, guns);
|
|
});
|
|
|
|
void weaponswitch(gameent *d)
|
|
{
|
|
if(d->state!=CS_ALIVE) return;
|
|
int s = d->gunselect;
|
|
if(s!=GUN_PULSE && d->ammo[GUN_PULSE]) s = GUN_PULSE;
|
|
else if(s!=GUN_RAIL && d->ammo[GUN_RAIL]) s = GUN_RAIL;
|
|
gunselect(s, d);
|
|
}
|
|
|
|
ICOMMAND(weapon, "V", (tagval *args, int numargs),
|
|
{
|
|
if(player1->state!=CS_ALIVE) return;
|
|
loopi(3)
|
|
{
|
|
const char *name = i < numargs ? args[i].getstr() : "";
|
|
if(name[0])
|
|
{
|
|
int gun = getweapon(name);
|
|
if(validgun(gun) && gun != player1->gunselect && player1->ammo[gun]) { gunselect(gun, player1); return; }
|
|
} else { weaponswitch(player1); return; }
|
|
}
|
|
playsound(S_NOAMMO);
|
|
});
|
|
|
|
void offsetray(const vec &from, const vec &to, int spread, float range, vec &dest)
|
|
{
|
|
vec offset;
|
|
do offset = vec(rndscale(1), rndscale(1), rndscale(1)).sub(0.5f);
|
|
while(offset.squaredlen() > 0.5f*0.5f);
|
|
offset.mul((to.dist(from)/1024)*spread);
|
|
offset.z /= 2;
|
|
dest = vec(offset).add(to);
|
|
if(dest != from)
|
|
{
|
|
vec dir = vec(dest).sub(from).normalize();
|
|
raycubepos(from, dir, dest, range, RAY_CLIPMAT|RAY_ALPHAPOLY);
|
|
}
|
|
}
|
|
|
|
void createrays(int atk, const vec &from, const vec &to) // create random spread of rays
|
|
{
|
|
loopi(attacks[atk].rays) offsetray(from, to, attacks[atk].spread, attacks[atk].range, rays[i]);
|
|
}
|
|
|
|
enum { BNC_GIBS, BNC_DEBRIS };
|
|
|
|
struct bouncer : physent
|
|
{
|
|
int lifetime, bounces;
|
|
float lastyaw, roll;
|
|
bool local;
|
|
gameent *owner;
|
|
int bouncetype, variant;
|
|
vec offset;
|
|
int offsetmillis;
|
|
int id;
|
|
|
|
bouncer() : bounces(0), roll(0), variant(0)
|
|
{
|
|
type = ENT_BOUNCE;
|
|
}
|
|
};
|
|
|
|
vector<bouncer *> bouncers;
|
|
|
|
void newbouncer(const vec &from, const vec &to, bool local, int id, gameent *owner, int type, int lifetime, int speed)
|
|
{
|
|
bouncer &bnc = *bouncers.add(new bouncer);
|
|
bnc.o = from;
|
|
bnc.radius = bnc.xradius = bnc.yradius = type==BNC_DEBRIS ? 0.5f : 1.5f;
|
|
bnc.eyeheight = bnc.radius;
|
|
bnc.aboveeye = bnc.radius;
|
|
bnc.lifetime = lifetime;
|
|
bnc.local = local;
|
|
bnc.owner = owner;
|
|
bnc.bouncetype = type;
|
|
bnc.id = local ? lastmillis : id;
|
|
|
|
switch(type)
|
|
{
|
|
case BNC_DEBRIS: bnc.variant = rnd(4); break;
|
|
case BNC_GIBS: bnc.variant = rnd(3); break;
|
|
}
|
|
|
|
vec dir(to);
|
|
dir.sub(from).safenormalize();
|
|
bnc.vel = dir;
|
|
bnc.vel.mul(speed);
|
|
|
|
avoidcollision(&bnc, dir, owner, 0.1f);
|
|
|
|
bnc.offset = from;
|
|
bnc.offset.sub(bnc.o);
|
|
bnc.offsetmillis = OFFSETMILLIS;
|
|
|
|
bnc.resetinterp();
|
|
}
|
|
|
|
void bounced(physent *d, const vec &surface)
|
|
{
|
|
if(d->type != ENT_BOUNCE) return;
|
|
bouncer *b = (bouncer *)d;
|
|
if(b->bouncetype != BNC_GIBS || b->bounces >= 2) return;
|
|
b->bounces++;
|
|
addstain(STAIN_BLOOD, vec(b->o).sub(vec(surface).mul(b->radius)), surface, 2.96f/b->bounces, bvec(0x60, 0xFF, 0xFF), rnd(4));
|
|
}
|
|
|
|
void updatebouncers(int time)
|
|
{
|
|
loopv(bouncers)
|
|
{
|
|
bouncer &bnc = *bouncers[i];
|
|
vec old(bnc.o);
|
|
bool stopped = false;
|
|
// cheaper variable rate physics for debris, gibs, etc.
|
|
for(int rtime = time; rtime > 0;)
|
|
{
|
|
int qtime = min(30, rtime);
|
|
rtime -= qtime;
|
|
if((bnc.lifetime -= qtime)<0 || bounce(&bnc, qtime/1000.0f, 0.6f, 0.5f, 1)) { stopped = true; break; }
|
|
}
|
|
if(stopped)
|
|
{
|
|
delete bouncers.remove(i--);
|
|
}
|
|
else
|
|
{
|
|
bnc.roll += old.sub(bnc.o).magnitude()/(4*RAD);
|
|
bnc.offsetmillis = max(bnc.offsetmillis-time, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void removebouncers(gameent *owner)
|
|
{
|
|
loopv(bouncers) if(bouncers[i]->owner==owner) { delete bouncers[i]; bouncers.remove(i--); }
|
|
}
|
|
|
|
void clearbouncers() { bouncers.deletecontents(); }
|
|
|
|
struct projectile
|
|
{
|
|
vec dir, o, from, to, offset;
|
|
float speed;
|
|
gameent *owner;
|
|
int atk;
|
|
bool local;
|
|
int offsetmillis;
|
|
int id;
|
|
};
|
|
vector<projectile> projs;
|
|
|
|
void clearprojectiles() { projs.shrink(0); }
|
|
|
|
void newprojectile(const vec &from, const vec &to, float speed, bool local, int id, gameent *owner, int atk)
|
|
{
|
|
projectile &p = projs.add();
|
|
p.dir = vec(to).sub(from).safenormalize();
|
|
p.o = from;
|
|
p.from = from;
|
|
p.to = to;
|
|
p.offset = hudgunorigin(attacks[atk].gun, from, to, owner);
|
|
p.offset.sub(from);
|
|
p.speed = speed;
|
|
p.local = local;
|
|
p.owner = owner;
|
|
p.atk = atk;
|
|
p.offsetmillis = OFFSETMILLIS;
|
|
p.id = local ? lastmillis : id;
|
|
}
|
|
|
|
void removeprojectiles(gameent *owner)
|
|
{
|
|
// can't use loopv here due to strange GCC optimizer bug
|
|
int len = projs.length();
|
|
loopi(len) if(projs[i].owner==owner) { projs.remove(i--); len--; }
|
|
}
|
|
|
|
VARP(blood, 0, 1, 1);
|
|
|
|
void damageeffect(int damage, gameent *d, bool thirdperson)
|
|
{
|
|
vec p = d->o;
|
|
p.z += 0.6f*(d->eyeheight + d->aboveeye) - d->eyeheight;
|
|
if(blood) particle_splash(PART_BLOOD, max(damage/10, rnd(3)+1), 1000, p, 0x60FFFF, 2.96f);
|
|
#if 0
|
|
if(thirdperson) particle_textcopy(d->abovehead(), tempformatstring("%d", damage), PART_TEXT, 2000, 0xFF4B19, 4.0f, -8);
|
|
#endif
|
|
}
|
|
|
|
void spawnbouncer(const vec &p, const vec &vel, gameent *d, int type)
|
|
{
|
|
vec to(rnd(100)-50, rnd(100)-50, rnd(100)-50);
|
|
if(to.iszero()) to.z += 1;
|
|
to.normalize();
|
|
to.add(p);
|
|
newbouncer(p, to, true, 0, d, type, rnd(1000)+1000, rnd(100)+20);
|
|
}
|
|
|
|
void gibeffect(int damage, const vec &vel, gameent *d)
|
|
{
|
|
#if 0
|
|
if(!blood || !maxgibs || damage < 0) return;
|
|
vec from = d->abovehead();
|
|
loopi(rnd(maxgibs)+1) spawnbouncer(from, vel, d, BNC_GIBS);
|
|
#endif
|
|
}
|
|
|
|
void hit(int damage, dynent *d, gameent *at, const vec &vel, int atk, float info1, int info2 = 1)
|
|
{
|
|
if(at==player1 && d!=at)
|
|
{
|
|
extern int hitsound;
|
|
if(hitsound && lasthit != lastmillis) playsound(S_HIT);
|
|
lasthit = lastmillis;
|
|
}
|
|
|
|
gameent *f = (gameent *)d;
|
|
|
|
f->lastpain = lastmillis;
|
|
if(at->type==ENT_PLAYER && !isteam(at->team, f->team)) at->totaldamage += damage;
|
|
|
|
if(!m_mp(gamemode) || f==at) f->hitpush(damage, vel, at, atk);
|
|
|
|
if(!m_mp(gamemode)) damaged(damage, f, at);
|
|
else
|
|
{
|
|
hitmsg &h = hits.add();
|
|
h.target = f->clientnum;
|
|
h.lifesequence = f->lifesequence;
|
|
h.info1 = int(info1*DMF);
|
|
h.info2 = info2;
|
|
h.dir = f==at ? ivec(0, 0, 0) : ivec(vec(vel).mul(DNF));
|
|
if(at==player1)
|
|
{
|
|
damageeffect(damage, f);
|
|
if(f==player1)
|
|
{
|
|
damageblend(damage);
|
|
damagecompass(damage, at ? at->o : f->o);
|
|
playsound(S_PAIN2);
|
|
}
|
|
else playsound(S_PAIN1, &f->o);
|
|
}
|
|
}
|
|
}
|
|
|
|
void hitpush(int damage, dynent *d, gameent *at, vec &from, vec &to, int atk, int rays)
|
|
{
|
|
hit(damage, d, at, vec(to).sub(from).safenormalize(), atk, from.dist(to), rays);
|
|
}
|
|
|
|
float projdist(dynent *o, vec &dir, const vec &v, const vec &vel)
|
|
{
|
|
vec middle = o->o;
|
|
middle.z += (o->aboveeye-o->eyeheight)/2;
|
|
dir = vec(middle).sub(v).add(vec(vel).mul(5)).safenormalize();
|
|
|
|
float low = min(o->o.z - o->eyeheight + o->radius, middle.z),
|
|
high = max(o->o.z + o->aboveeye - o->radius, middle.z);
|
|
vec closest(o->o.x, o->o.y, clamp(v.z, low, high));
|
|
return max(closest.dist(v) - o->radius, 0.0f);
|
|
}
|
|
|
|
void radialeffect(dynent *o, const vec &v, const vec &vel, int qdam, gameent *at, int atk)
|
|
{
|
|
if(o->state!=CS_ALIVE) return;
|
|
vec dir;
|
|
float dist = projdist(o, dir, v, vel);
|
|
if(dist<attacks[atk].exprad)
|
|
{
|
|
float damage = qdam*(1-dist/EXP_DISTSCALE/attacks[atk].exprad);
|
|
if(o==at) damage /= EXP_SELFDAMDIV;
|
|
if(damage > 0) hit(max(int(damage), 1), o, at, dir, atk, dist);
|
|
}
|
|
}
|
|
|
|
void explode(bool local, gameent *owner, const vec &v, const vec &vel, dynent *safe, int damage, int atk)
|
|
{
|
|
particle_splash(PART_SPARK, 200, 300, v, 0x50CFE5, 0.45f);
|
|
playsound(S_PULSEEXPLODE, &v);
|
|
particle_fireball(v, 1.15f*attacks[atk].exprad, PART_PULSE_BURST, int(attacks[atk].exprad*20), 0x50CFE5, 4.0f);
|
|
vec debrisorigin = vec(v).sub(vec(vel).mul(5));
|
|
adddynlight(safe ? v : debrisorigin, 2*attacks[atk].exprad, vec(1.0f, 3.0f, 4.0f), 350, 40, 0, attacks[atk].exprad/2, vec(0.5f, 1.5f, 2.0f));
|
|
#if 0
|
|
int numdebris = maxdebris > MINDEBRIS ? rnd(maxdebris-MINDEBRIS)+MINDEBRIS : min(maxdebris, MINDEBRIS);
|
|
if(numdebris)
|
|
{
|
|
vec debrisvel = vec(vel).neg();
|
|
loopi(numdebris)
|
|
spawnbouncer(debrisorigin, debrisvel, owner, BNC_DEBRIS);
|
|
}
|
|
#endif
|
|
if(!local) return;
|
|
int numdyn = numdynents();
|
|
loopi(numdyn)
|
|
{
|
|
dynent *o = iterdynents(i);
|
|
if(o->o.reject(v, o->radius + attacks[atk].exprad) || o==safe) continue;
|
|
radialeffect(o, v, vel, damage, owner, atk);
|
|
}
|
|
}
|
|
|
|
void pulsestain(const projectile &p, const vec &pos)
|
|
{
|
|
vec dir = vec(p.dir).neg();
|
|
float rad = attacks[p.atk].exprad*0.75f;
|
|
addstain(STAIN_PULSE_SCORCH, pos, dir, rad);
|
|
addstain(STAIN_PULSE_GLOW, pos, dir, rad, 0x50CFE5);
|
|
}
|
|
|
|
void projsplash(projectile &p, const vec &v, dynent *safe)
|
|
{
|
|
explode(p.local, p.owner, v, p.dir, safe, attacks[p.atk].damage, p.atk);
|
|
pulsestain(p, v);
|
|
}
|
|
|
|
void explodeeffects(int atk, gameent *d, bool local, int id)
|
|
{
|
|
if(local) return;
|
|
switch(atk)
|
|
{
|
|
case ATK_PULSE_SHOOT:
|
|
loopv(projs)
|
|
{
|
|
projectile &p = projs[i];
|
|
if(p.atk == atk && p.owner == d && p.id == id && !p.local)
|
|
{
|
|
vec pos = vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS)).add(p.o);
|
|
explode(p.local, p.owner, pos, p.dir, NULL, 0, atk);
|
|
pulsestain(p, pos);
|
|
projs.remove(i);
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool projdamage(dynent *o, projectile &p, const vec &v)
|
|
{
|
|
if(o->state!=CS_ALIVE) return false;
|
|
if(!intersect(o, p.o, v, attacks[p.atk].margin)) return false;
|
|
projsplash(p, v, o);
|
|
vec dir;
|
|
projdist(o, dir, v, p.dir);
|
|
hit(attacks[p.atk].damage, o, p.owner, dir, p.atk, 0);
|
|
return true;
|
|
}
|
|
|
|
void updateprojectiles(int time)
|
|
{
|
|
if(projs.empty()) return;
|
|
gameent *noside = hudplayer();
|
|
loopv(projs)
|
|
{
|
|
projectile &p = projs[i];
|
|
p.offsetmillis = max(p.offsetmillis-time, 0);
|
|
vec dv;
|
|
float dist = p.to.dist(p.o, dv);
|
|
dv.mul(time/max(dist*1000/p.speed, float(time)));
|
|
vec v = vec(p.o).add(dv);
|
|
bool exploded = false;
|
|
hits.setsize(0);
|
|
if(p.local)
|
|
{
|
|
vec halfdv = vec(dv).mul(0.5f), bo = vec(p.o).add(halfdv);
|
|
float br = max(fabs(halfdv.x), fabs(halfdv.y)) + 1 + attacks[p.atk].margin;
|
|
loopj(numdynents())
|
|
{
|
|
dynent *o = iterdynents(j);
|
|
if(p.owner==o || o->o.reject(bo, o->radius + br)) continue;
|
|
if(projdamage(o, p, v)) { exploded = true; break; }
|
|
}
|
|
}
|
|
if(!exploded)
|
|
{
|
|
if(dist<4)
|
|
{
|
|
if(p.o!=p.to) // if original target was moving, reevaluate endpoint
|
|
{
|
|
if(raycubepos(p.o, p.dir, p.to, 0, RAY_CLIPMAT|RAY_ALPHAPOLY)>=4) continue;
|
|
}
|
|
projsplash(p, v, NULL);
|
|
exploded = true;
|
|
}
|
|
else
|
|
{
|
|
vec pos = vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS)).add(v);
|
|
particle_splash(PART_PULSE_FRONT, 1, 1, pos, 0x50CFE5, 2.4f, 150, 20);
|
|
if(p.owner != noside)
|
|
{
|
|
float len = min(20.0f, vec(p.offset).add(p.from).dist(pos));
|
|
vec dir = vec(dv).normalize(),
|
|
tail = vec(dir).mul(-len).add(pos),
|
|
head = vec(dir).mul(2.4f).add(pos);
|
|
particle_flare(tail, head, 1, PART_PULSE_SIDE, 0x50CFE5, 2.5f);
|
|
}
|
|
}
|
|
}
|
|
if(exploded)
|
|
{
|
|
if(p.local)
|
|
addmsg(N_EXPLODE, "rci3iv", p.owner, lastmillis-maptime, p.atk, p.id-maptime,
|
|
hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf());
|
|
projs.remove(i--);
|
|
}
|
|
else p.o = v;
|
|
}
|
|
}
|
|
|
|
void railhit(const vec &from, const vec &to, bool stain = true)
|
|
{
|
|
vec dir = vec(from).sub(to).safenormalize();
|
|
if(stain)
|
|
{
|
|
addstain(STAIN_RAIL_HOLE, to, dir, 2.0f);
|
|
addstain(STAIN_RAIL_GLOW, to, dir, 2.5f, 0x50CFE5);
|
|
}
|
|
adddynlight(vec(to).madd(dir, 4), 10, vec(0.25f, 0.75f, 1.0f), 225, 75);
|
|
}
|
|
|
|
void shoteffects(int atk, const vec &from, const vec &to, gameent *d, bool local, int id, int prevaction) // create visual effect from a shot
|
|
{
|
|
int gun = attacks[atk].gun;
|
|
switch(atk)
|
|
{
|
|
case ATK_PULSE_SHOOT:
|
|
if(d->muzzle.x >= 0)
|
|
particle_flare(d->muzzle, d->muzzle, 140, PART_PULSE_MUZZLE_FLASH, 0x50CFE5, 3.50f, d);
|
|
newprojectile(from, to, attacks[atk].projspeed, local, id, d, atk);
|
|
break;
|
|
|
|
case ATK_RAIL_SHOOT:
|
|
particle_splash(PART_SPARK, 200, 250, to, 0x50CFE5, 0.45f);
|
|
particle_flare(hudgunorigin(gun, from, to, d), to, 500, PART_RAIL_TRAIL, 0x50CFE5, 0.5f);
|
|
if(d->muzzle.x >= 0)
|
|
particle_flare(d->muzzle, d->muzzle, 140, PART_RAIL_MUZZLE_FLASH, 0x50CFE5, 2.75f, d);
|
|
adddynlight(hudgunorigin(gun, d->o, to, d), 35, vec(0.25f, 0.75f, 1.0f), 75, 75, DL_FLASH, 0, vec(0, 0, 0), d);
|
|
if(!local) railhit(from, to);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if(d==hudplayer()) playsound(attacks[atk].hudsound, NULL);
|
|
else playsound(attacks[atk].sound, &d->o);
|
|
}
|
|
|
|
void particletrack(physent *owner, vec &o, vec &d)
|
|
{
|
|
if(owner->type!=ENT_PLAYER) return;
|
|
gameent *pl = (gameent *)owner;
|
|
if(pl->muzzle.x < 0 || pl->lastattack < 0 || attacks[pl->lastattack].gun != pl->gunselect) return;
|
|
float dist = o.dist(d);
|
|
o = pl->muzzle;
|
|
if(dist <= 0) d = o;
|
|
else
|
|
{
|
|
vecfromyawpitch(owner->yaw, owner->pitch, 1, 0, d);
|
|
float newdist = raycube(owner->o, d, dist, RAY_CLIPMAT|RAY_ALPHAPOLY);
|
|
d.mul(min(newdist, dist)).add(owner->o);
|
|
}
|
|
}
|
|
|
|
void dynlighttrack(physent *owner, vec &o, vec &hud)
|
|
{
|
|
if(owner->type!=ENT_PLAYER) return;
|
|
gameent *pl = (gameent *)owner;
|
|
if(pl->muzzle.x < 0 || pl->lastattack < 0 || attacks[pl->lastattack].gun != pl->gunselect) return;
|
|
o = pl->muzzle;
|
|
hud = owner == hudplayer() ? vec(pl->o).add(vec(0, 0, 2)) : pl->muzzle;
|
|
}
|
|
|
|
float intersectdist = 1e16f;
|
|
|
|
bool intersect(dynent *d, const vec &from, const vec &to, float margin, float &dist) // if lineseg hits entity bounding box
|
|
{
|
|
vec bottom(d->o), top(d->o);
|
|
bottom.z -= d->eyeheight + margin;
|
|
top.z += d->aboveeye + margin;
|
|
return linecylinderintersect(from, to, bottom, top, d->radius + margin, dist);
|
|
}
|
|
|
|
dynent *intersectclosest(const vec &from, const vec &to, gameent *at, float margin, float &bestdist)
|
|
{
|
|
dynent *best = NULL;
|
|
bestdist = 1e16f;
|
|
loopi(numdynents())
|
|
{
|
|
dynent *o = iterdynents(i);
|
|
if(o==at || o->state!=CS_ALIVE) continue;
|
|
float dist;
|
|
if(!intersect(o, from, to, margin, dist)) continue;
|
|
if(dist<bestdist)
|
|
{
|
|
best = o;
|
|
bestdist = dist;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
void shorten(const vec &from, vec &target, float dist)
|
|
{
|
|
target.sub(from).mul(min(1.0f, dist)).add(from);
|
|
}
|
|
|
|
void raydamage(vec &from, vec &to, gameent *d, int atk)
|
|
{
|
|
dynent *o;
|
|
float dist;
|
|
int maxrays = attacks[atk].rays, margin = attacks[atk].margin;
|
|
if(attacks[atk].rays > 1)
|
|
{
|
|
dynent *hits[MAXRAYS];
|
|
loopi(maxrays)
|
|
{
|
|
if((hits[i] = intersectclosest(from, rays[i], d, margin, dist)))
|
|
{
|
|
shorten(from, rays[i], dist);
|
|
railhit(from, rays[i], false);
|
|
}
|
|
else railhit(from, rays[i]);
|
|
}
|
|
loopi(maxrays) if(hits[i])
|
|
{
|
|
o = hits[i];
|
|
hits[i] = NULL;
|
|
int numhits = 1;
|
|
for(int j = i+1; j < maxrays; j++) if(hits[j] == o)
|
|
{
|
|
hits[j] = NULL;
|
|
numhits++;
|
|
}
|
|
hitpush(numhits*attacks[atk].damage, o, d, from, to, atk, numhits);
|
|
}
|
|
}
|
|
else if((o = intersectclosest(from, to, d, margin, dist)))
|
|
{
|
|
shorten(from, to, dist);
|
|
railhit(from, to, false);
|
|
hitpush(attacks[atk].damage, o, d, from, to, atk, 1);
|
|
}
|
|
else if(attacks[atk].action!=ACT_MELEE) railhit(from, to);
|
|
}
|
|
|
|
void shoot(gameent *d, const vec &targ)
|
|
{
|
|
int prevaction = d->lastaction, attacktime = lastmillis-prevaction;
|
|
if(attacktime<d->gunwait) return;
|
|
d->gunwait = 0;
|
|
if(!d->attacking) return;
|
|
int gun = d->gunselect, act = d->attacking, atk = guns[gun].attacks[act];
|
|
d->lastaction = lastmillis;
|
|
d->lastattack = atk;
|
|
if(!d->ammo[gun])
|
|
{
|
|
if(d==player1)
|
|
{
|
|
msgsound(S_NOAMMO, d);
|
|
d->gunwait = 600;
|
|
d->lastattack = -1;
|
|
weaponswitch(d);
|
|
}
|
|
return;
|
|
}
|
|
d->ammo[gun] -= attacks[atk].use;
|
|
|
|
vec from = d->o, to = targ, dir = vec(to).sub(from).safenormalize();
|
|
float dist = to.dist(from);
|
|
if(!(d->physstate >= PHYS_SLOPE && d->crouching && d->crouched()))
|
|
{
|
|
vec kickback = vec(dir).mul(attacks[atk].kickamount*-2.5f);
|
|
d->vel.add(kickback);
|
|
}
|
|
float shorten = attacks[atk].range && dist > attacks[atk].range ? attacks[atk].range : 0,
|
|
barrier = raycube(d->o, dir, dist, RAY_CLIPMAT|RAY_ALPHAPOLY);
|
|
if(barrier > 0 && barrier < dist && (!shorten || barrier < shorten))
|
|
shorten = barrier;
|
|
if(shorten) to = vec(dir).mul(shorten).add(from);
|
|
|
|
if(attacks[atk].rays > 1) createrays(atk, from, to);
|
|
else if(attacks[atk].spread) offsetray(from, to, attacks[atk].spread, attacks[atk].range, to);
|
|
|
|
hits.setsize(0);
|
|
|
|
if(!attacks[atk].projspeed) raydamage(from, to, d, atk);
|
|
|
|
shoteffects(atk, from, to, d, true, 0, prevaction);
|
|
|
|
if(d==player1 || d->ai)
|
|
{
|
|
addmsg(N_SHOOT, "rci2i6iv", d, lastmillis-maptime, atk,
|
|
(int)(from.x*DMF), (int)(from.y*DMF), (int)(from.z*DMF),
|
|
(int)(to.x*DMF), (int)(to.y*DMF), (int)(to.z*DMF),
|
|
hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf());
|
|
}
|
|
|
|
d->gunwait = attacks[atk].attackdelay;
|
|
if(attacks[atk].action != ACT_MELEE && d->ai) d->gunwait += int(d->gunwait*(((101-d->skill)+rnd(111-d->skill))/100.f));
|
|
d->totalshots += attacks[atk].damage*attacks[atk].rays;
|
|
}
|
|
|
|
void adddynlights()
|
|
{
|
|
loopv(projs)
|
|
{
|
|
projectile &p = projs[i];
|
|
if(p.atk!=ATK_PULSE_SHOOT) continue;
|
|
vec pos(p.o);
|
|
pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS)));
|
|
adddynlight(pos, 20, vec(0.25f, 0.75f, 1.0f));
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
static const char * const gibnames[3] = { "gibs/gib01", "gibs/gib02", "gibs/gib03" };
|
|
static const char * const debrisnames[4] = { "debris/debris01", "debris/debris02", "debris/debris03", "debris/debris04" };
|
|
#endif
|
|
|
|
void preloadbouncers()
|
|
{
|
|
#if 0
|
|
loopi(sizeof(gibnames)/sizeof(gibnames[0])) preloadmodel(gibnames[i]);
|
|
loopi(sizeof(debrisnames)/sizeof(debrisnames[0])) preloadmodel(debrisnames[i]);
|
|
#endif
|
|
}
|
|
|
|
void renderbouncers()
|
|
{
|
|
float yaw, pitch;
|
|
loopv(bouncers)
|
|
{
|
|
bouncer &bnc = *bouncers[i];
|
|
vec pos(bnc.o);
|
|
pos.add(vec(bnc.offset).mul(bnc.offsetmillis/float(OFFSETMILLIS)));
|
|
vec vel(bnc.vel);
|
|
if(vel.magnitude() <= 25.0f) yaw = bnc.lastyaw;
|
|
else
|
|
{
|
|
vectoyawpitch(vel, yaw, pitch);
|
|
yaw += 90;
|
|
bnc.lastyaw = yaw;
|
|
}
|
|
pitch = -bnc.roll;
|
|
const char *mdl = NULL;
|
|
int cull = MDL_CULL_VFC|MDL_CULL_DIST|MDL_CULL_OCCLUDED;
|
|
float fade = 1;
|
|
if(bnc.lifetime < 250) fade = bnc.lifetime/250.0f;
|
|
switch(bnc.bouncetype)
|
|
{
|
|
#if 0
|
|
case BNC_GIBS: mdl = gibnames[bnc.variant]; break;
|
|
case BNC_DEBRIS: mdl = debrisnames[bnc.variant]; break;
|
|
#endif
|
|
default: continue;
|
|
}
|
|
rendermodel(mdl, ANIM_MAPMODEL|ANIM_LOOP, pos, yaw, pitch, 0, cull, NULL, NULL, 0, 0, fade);
|
|
}
|
|
}
|
|
|
|
void renderprojectiles()
|
|
{
|
|
}
|
|
|
|
void removeweapons(gameent *d)
|
|
{
|
|
removebouncers(d);
|
|
removeprojectiles(d);
|
|
}
|
|
|
|
void updateweapons(int curtime)
|
|
{
|
|
updateprojectiles(curtime);
|
|
if(player1->clientnum>=0 && player1->state==CS_ALIVE) shoot(player1, worldpos); // only shoot when connected to server
|
|
updatebouncers(curtime); // need to do this after the player shoots so bouncers don't end up inside player's BB next frame
|
|
}
|
|
|
|
void avoidweapons(ai::avoidset &obstacles, float radius)
|
|
{
|
|
loopv(projs)
|
|
{
|
|
projectile &p = projs[i];
|
|
obstacles.avoidnear(NULL, p.o.z + attacks[p.atk].exprad + 1, p.o, radius + attacks[p.atk].exprad);
|
|
}
|
|
}
|
|
};
|
|
|