// 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 hits; ICOMMAND(getweapon, "", (), intret(player1->gunselect)); void gunselect(int gun, fpsent *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) { const char *abbrevs[] = { "FI", "SG", "CG", "RL", "RI", "GL", "PI" }; if(isdigit(name[0])) return parseint(name); else loopi(sizeof(abbrevs)/sizeof(abbrevs[0])) if(!strcasecmp(abbrevs[i], name)) return i; return -1; } void setweapon(const char *name, bool force = false) { int gun = getweapon(name); if(player1->state!=CS_ALIVE || gunGUN_PISTOL) 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 && gunammo[gun])) { gunselect(gun, player1); return; } } playsound(S_NOAMMO); } ICOMMAND(cycleweapon, "V", (tagval *args, int numargs), { int numguns = min(numargs, 7); int guns[7]; loopi(numguns) guns[i] = getweapon(args[i].getstr()); cycleweapon(numguns, guns); }); void weaponswitch(fpsent *d) { if(d->state!=CS_ALIVE) return; int s = d->gunselect; if (s!=GUN_CG && d->ammo[GUN_CG]) s = GUN_CG; else if(s!=GUN_RL && d->ammo[GUN_RL]) s = GUN_RL; else if(s!=GUN_SG && d->ammo[GUN_SG]) s = GUN_SG; else if(s!=GUN_RIFLE && d->ammo[GUN_RIFLE]) s = GUN_RIFLE; else if(s!=GUN_GL && d->ammo[GUN_GL]) s = GUN_GL; else if(s!=GUN_PISTOL && d->ammo[GUN_PISTOL]) s = GUN_PISTOL; else s = GUN_FIST; gunselect(s, d); } ICOMMAND(weapon, "V", (tagval *args, int numargs), { if(player1->state!=CS_ALIVE) return; loopi(7) { const char *name = i < numargs ? args[i].getstr() : ""; if(name[0]) { int gun = getweapon(name); if(gun >= GUN_FIST && gun <= GUN_PISTOL && 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 gun, const vec &from, const vec &to) { // create random spread of rays { loopi(guns[gun].rays) offsetray(from, to, guns[gun].spread, guns[gun].range, rays[i]); } enum { BNC_GRENADE }; struct bouncer : physent { int lifetime, bounces; float lastyaw, roll; bool local; fpsent *owner; int bouncetype, variant; vec offset; int offsetmillis; float offsetheight; int id; entitylight light; bouncer() : bounces(0), roll(0), variant(0) { type = ENT_BOUNCE; } vec offsetpos() { vec pos(o); if(offsetmillis > 0) { pos.add(vec(offset).mul(offsetmillis/float(OFFSETMILLIS))); if(offsetheight >= 0) pos.z = max(pos.z, o.z - max(offsetheight - eyeheight, 0.0f)); } return pos; } void limitoffset() { if(bouncetype == BNC_GRENADE && offsetmillis > 0 && offset.z < 0) offsetheight = raycube(vec(o.x + offset.x, o.y + offset.y, o.z), vec(0, 0, -1), -offset.z); else offsetheight = -1; } }; vector bouncers; vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d); void newbouncer(const vec &from, const vec &to, bool local, int id, fpsent *owner, int type, int lifetime, int speed, entitylight *light = NULL) { bouncer &bnc = *bouncers.add(new bouncer); bnc.o = from; bnc.radius = bnc.xradius = bnc.yradius = 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; if(light) bnc.light = *light; bnc.collidetype = COLLIDE_ELLIPSE_PRECISE; vec dir(to); dir.sub(from).safenormalize(); bnc.vel = dir; bnc.vel.mul(speed); avoidcollision(&bnc, dir, owner, 0.1f); if(type==BNC_GRENADE) { bnc.offset = hudgunorigin(GUN_GL, from, to, owner); if(owner==followingplayer(player1) && !isthirdperson()) bnc.offset.sub(owner->o).rescale(16).add(owner->o); } else bnc.offset = from; bnc.offset.sub(bnc.o); bnc.offsetmillis = OFFSETMILLIS; bnc.limitoffset(); bnc.resetinterp(); } void bounced(physent *d, const vec &surface) { if(d->type != ENT_BOUNCE) return; bouncer *b = (bouncer *)d; if(b->bounces >= 2) return; b->bounces++; adddecal(DECAL_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]; if(bnc.bouncetype==BNC_GRENADE && bnc.vel.magnitude() > 50.0f) { vec pos = bnc.offsetpos(); regular_particle_splash(PART_SMOKE, 1, 150, pos, 0x404040, 2.4f, 50, -20); } vec old(bnc.o); bool stopped = false; if(bnc.bouncetype==BNC_GRENADE) stopped = bounce(&bnc, 0.6f, 0.5f, 0.8f) || (bnc.lifetime -= time)<0; if(stopped) { if(bnc.bouncetype==BNC_GRENADE) { int qdam = guns[GUN_GL].damage*(bnc.owner->quadmillis ? 4 : 1); hits.setsize(0); explode(bnc.local, bnc.owner, bnc.o, NULL, qdam, GUN_GL); adddecal(DECAL_SCORCH, bnc.o, vec(0, 0, 1), guns[GUN_GL].exprad/2); if(bnc.local) addmsg(N_EXPLODE, "rci3iv", bnc.owner, lastmillis-maptime, GUN_GL, bnc.id-maptime, hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf()); } delete bouncers.remove(i--); } else { bnc.roll += old.sub(bnc.o).magnitude()/(4*RAD); bnc.offsetmillis = max(bnc.offsetmillis-time, 0); bnc.limitoffset(); } } } void removebouncers(fpsent *owner) { loopv(bouncers) if(bouncers[i]->owner==owner) { delete bouncers[i]; bouncers.remove(i--); } } void clearbouncers() { bouncers.deletecontents(); } struct projectile { vec dir, o, to, offset; float speed; fpsent *owner; int gun; bool local; int offsetmillis; int id; entitylight light; }; vector projs; void clearprojectiles() { projs.shrink(0); } void newprojectile(const vec &from, const vec &to, float speed, bool local, int id, fpsent *owner, int gun) { projectile &p = projs.add(); p.dir = vec(to).sub(from).safenormalize(); p.o = from; p.to = to; p.offset = hudgunorigin(gun, from, to, owner); p.offset.sub(from); p.speed = speed; p.local = local; p.owner = owner; p.gun = gun; p.offsetmillis = OFFSETMILLIS; p.id = local ? lastmillis : id; } void removeprojectiles(fpsent *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, fpsent *d, bool thirdperson) { vec p = d->o; p.z += 0.6f*(d->eyeheight + d->aboveeye) - d->eyeheight; if(blood) particle_splash(PART_BLOOD, damage/10, 1000, p, 0x60FFFF, 2.96f); if(thirdperson) { defformatstring(ds, "%d", damage); particle_textcopy(d->abovehead(), ds, PART_TEXT, 2000, 0xFF4B19, 4.0f, -8); } } //~void spawnbouncer(const vec &p, const vec &vel, fpsent *d, int type, entitylight *light = NULL) //~{ //~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, light); //~} void hit(int damage, dynent *d, fpsent *at, const vec &vel, int gun, float info1, int info2 = 1) { if(at==player1 && d!=at) { extern int hitsound; if(hitsound && lasthit != lastmillis) playsound(S_HIT); lasthit = lastmillis; } fpsent *f = (fpsent *)d; f->lastpain = lastmillis; if(at->type==ENT_PLAYER && !isteam(at->team, f->team)) at->totaldamage += damage; if(f->type==ENT_AI || !m_mp(gamemode) || f==at) f->hitpush(damage, vel, at, gun); pwhit(gun, damage); 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) { damagecompass(damage, at ? at->o : f->o); playsound(S_PAIN6); } else playsound(S_PAIN1+rnd(5), &f->o); } } } void hitpush(int damage, dynent *d, fpsent *at, vec &from, vec &to, int gun, int rays) { hit(damage, d, at, vec(to).sub(from).safenormalize(), gun, from.dist(to), rays); } float projdist(dynent *o, vec &dir, const vec &v) { vec middle = o->o; middle.z += (o->aboveeye-o->eyeheight)/2; float dist = middle.dist(v, dir); dir.div(dist); if(dist<0) dist = 0; return dist; } void radialeffect(dynent *o, const vec &v, int qdam, fpsent *at, int gun) { if(o->state!=CS_ALIVE) return; vec dir; float dist = projdist(o, dir, v); if(disto.reject(v, o->radius + guns[gun].exprad) || o==safe) continue; radialeffect(o, v, damage, owner, gun); } } void projsplash(projectile &p, vec &v, dynent *safe, int damage) { if(guns[p.gun].part) { particle_splash(PART_SPARK, 100, 200, v, 0xB49B4B, 0.24f); playsound(S_FEXPLODE, &v); // no push? } else { explode(p.local, p.owner, v, safe, damage, GUN_RL); adddecal(DECAL_SCORCH, v, vec(p.dir).neg(), guns[p.gun].exprad/2); } } void explodeeffects(int gun, fpsent *d, bool local, int id) { if(local) return; switch(gun) { case GUN_RL: loopv(projs) { projectile &p = projs[i]; if(p.gun == gun && p.owner == d && p.id == id && !p.local) { vec pos(p.o); pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS))); explode(p.local, p.owner, pos, NULL, 0, GUN_RL); adddecal(DECAL_SCORCH, pos, vec(p.dir).neg(), guns[gun].exprad/2); projs.remove(i); break; } } break; case GUN_GL: loopv(bouncers) { bouncer &b = *bouncers[i]; if(b.bouncetype == BNC_GRENADE && b.owner == d && b.id == id && !b.local) { vec pos = b.offsetpos(); explode(b.local, b.owner, pos, NULL, 0, GUN_GL); adddecal(DECAL_SCORCH, pos, vec(0, 0, 1), guns[gun].exprad/2); delete bouncers.remove(i); break; } } break; default: break; } } bool projdamage(dynent *o, projectile &p, vec &v, int qdam) { if(o->state!=CS_ALIVE) return false; if(!intersect(o, p.o, v)) return false; projsplash(p, v, o, qdam); vec dir; projdist(o, dir, v); hit(qdam, o, p.owner, dir, p.gun, 0); return true; } void updateprojectiles(int time) { loopv(projs) { projectile &p = projs[i]; p.offsetmillis = max(p.offsetmillis-time, 0); int qdam = guns[p.gun].damage*(p.owner->quadmillis ? 4 : 1); 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; loopj(numdynents()) { dynent *o = iterdynents(j); if(p.owner==o || o->o.reject(bo, o->radius + br)) continue; if(projdamage(o, p, v, qdam)) { 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, qdam); exploded = true; } else { vec pos(v); pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS))); regular_particle_splash(PART_SMOKE, 2, 300, pos, 0x404040, 2.4f, 50, -20); } } if(exploded) { if(p.local) addmsg(N_EXPLODE, "rci3iv", p.owner, lastmillis-maptime, p.gun, p.id-maptime, hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf()); projs.remove(i--); } else p.o = v; } } extern int chainsawhudgun; VARP(muzzleflash, 0, 1, 1); VARP(muzzlelight, 0, 1, 1); void shoteffects(int gun, const vec &from, const vec &to, fpsent *d, bool local, int id, int prevaction) { // create visual effect from a shot { int sound = guns[gun].sound, pspeed = 25; switch(gun) { case GUN_FIST: if(d->type==ENT_PLAYER && chainsawhudgun) sound = S_CHAINSAW_ATTACK; break; case GUN_SG: { if(!local) createrays(gun, from, to); if(muzzleflash && d->muzzle.x >= 0) particle_flare(d->muzzle, d->muzzle, 200, PART_MUZZLE_FLASH3, 0xFFFFFF, 2.75f, d); loopi(guns[gun].rays) { if(d->quadmillis) particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, rays[i], d), rays[i], 0x802010, 0.6f, 36); particle_splash(PART_SPARK, 20, 250, rays[i], 0xB49B4B, 0.24f); particle_flare(hudgunorigin(gun, from, rays[i], d), rays[i], 300, PART_STREAK, 0xFFC864, 0.28f); if(!local) adddecal(DECAL_BULLET, rays[i], vec(from).sub(rays[i]).safenormalize(), 2.0f); } if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 30, vec(0.5f, 0.375f, 0.25f), 100, 100, DL_FLASH, 0, vec(0, 0, 0), d); break; } case GUN_CG: case GUN_PISTOL: { particle_splash(PART_SPARK, 200, 250, to, 0xB49B4B, 0.24f); if(d->quadmillis) particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, to, d), to, 0x802010, 0.6f, 36); particle_flare(hudgunorigin(gun, from, to, d), to, 600, PART_STREAK, 0xFFC864, 0.28f); if(muzzleflash && d->muzzle.x >= 0) particle_flare(d->muzzle, d->muzzle, gun==GUN_CG ? 100 : 200, PART_MUZZLE_FLASH1, 0xFFFFFF, gun==GUN_CG ? 2.25f : 1.25f, d); if(!local) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), 2.0f); if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), gun==GUN_CG ? 30 : 15, vec(0.5f, 0.375f, 0.25f), gun==GUN_CG ? 50 : 100, gun==GUN_CG ? 50 : 100, DL_FLASH, 0, vec(0, 0, 0), d); break; } case GUN_RL: if(d->quadmillis) particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, to, d), to, 0x802010, 0.6f, 36); if(muzzleflash && d->muzzle.x >= 0) particle_flare(d->muzzle, d->muzzle, 250, PART_MUZZLE_FLASH2, 0xFFFFFF, 3.0f, d); pspeed = guns[gun].projspeed; newprojectile(from, to, (float)pspeed, local, id, d, gun); break; case GUN_GL: { float dist = from.dist(to); vec up = to; up.z += dist/8; if(muzzleflash && d->muzzle.x >= 0) particle_flare(d->muzzle, d->muzzle, 200, PART_MUZZLE_FLASH2, 0xFFFFFF, 1.5f, d); if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 20, vec(0.5f, 0.375f, 0.25f), 100, 100, DL_FLASH, 0, vec(0, 0, 0), d); newbouncer(from, up, local, id, d, BNC_GRENADE, guns[gun].ttl, guns[gun].projspeed); break; } case GUN_RIFLE: particle_splash(PART_SPARK, 200, 250, to, 0xB49B4B, 0.24f); if(d->quadmillis) particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, to, d), to, 0x802010, 0.6f, 36); particle_trail(PART_SMOKE, 500, hudgunorigin(gun, from, to, d), to, 0x404040, 0.6f, 18); if(muzzleflash && d->muzzle.x >= 0) particle_flare(d->muzzle, d->muzzle, 150, PART_MUZZLE_FLASH3, 0xFFFFFF, 1.25f, d); if(!local) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), 3.0f); if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 25, vec(0.5f, 0.375f, 0.25f), 75, 75, DL_FLASH, 0, vec(0, 0, 0), d); break; } bool looped = false; if(d->attacksound >= 0 && d->attacksound != sound) d->stopattacksound(); if(d->idlesound >= 0) d->stopidlesound(); fpsent *h = followingplayer(player1); switch(sound) { case S_CHAINSAW_ATTACK: if(d->attacksound >= 0) looped = true; d->attacksound = sound; d->attackchan = playsound(sound, d==h ? NULL : &d->o, NULL, 0, -1, 100, d->attackchan); break; default: playsound(sound, d==h ? NULL : &d->o); break; } if(d->quadmillis && lastmillis-prevaction>200 && !looped) playsound(S_ITEMPUP, d==h ? NULL : &d->o); } void particletrack(physent *owner, vec &o, vec &d) { if(owner->type!=ENT_PLAYER && owner->type!=ENT_AI) return; fpsent *pl = (fpsent *)owner; if(pl->muzzle.x < 0 || pl->lastattackgun != 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 && owner->type!=ENT_AI) return; fpsent *pl = (fpsent *)owner; if(pl->muzzle.x < 0 || pl->lastattackgun != pl->gunselect) return; o = pl->muzzle; hud = owner == followingplayer(player1) ? vec(pl->o).add(vec(0, 0, 2)) : pl->muzzle; } float intersectdist = 1e16f; bool intersect(dynent *d, const vec &from, const vec &to, float &dist) { // if lineseg hits entity bounding box { vec bottom(d->o), top(d->o); bottom.z -= d->eyeheight; top.z += d->aboveeye; return linecylinderintersect(from, to, bottom, top, d->radius, dist); } dynent *intersectclosest(const vec &from, const vec &to, fpsent *at, 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, dist)) continue; if(distgunselect].damage; if(d->quadmillis) qdam *= 4; dynent *o; float dist; if(guns[d->gunselect].rays > 1) { dynent *hits[MAXRAYS]; int maxrays = guns[d->gunselect].rays; loopi(maxrays) { if((hits[i] = intersectclosest(from, rays[i], d, dist))) shorten(from, rays[i], dist); else adddecal(DECAL_BULLET, rays[i], vec(from).sub(rays[i]).safenormalize(), 2.0f); } 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*qdam, o, d, from, to, d->gunselect, numhits); } } else if((o = intersectclosest(from, to, d, dist))) { shorten(from, to, dist); hitpush(qdam, o, d, from, to, d->gunselect, 1); } else if(d->gunselect!=GUN_FIST) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), d->gunselect==GUN_RIFLE ? 3.0f : 2.0f); } void shoot(fpsent *d, const vec &targ) { int prevaction = d->lastaction, attacktime = lastmillis-prevaction; if(attacktimegunwait) return; d->gunwait = 0; if((d==player1 || d->ai) && !d->attacking) return; d->lastaction = lastmillis; d->lastattackgun = d->gunselect; if(!d->ammo[d->gunselect]) { if(d==player1) { msgsound(S_NOAMMO, d); d->gunwait = 600; d->lastattackgun = -1; weaponswitch(d); } return; } if(d->gunselect) d->ammo[d->gunselect]--; pwshot(d->gunselect); /// PW vec from = d->o, to = targ, dir = vec(to).sub(from).safenormalize(); float dist = to.dist(from); vec kickback = vec(dir).mul(guns[d->gunselect].kickamount*-2.5f); d->vel.add(kickback); float shorten = 0; if(guns[d->gunselect].range && dist > guns[d->gunselect].range) shorten = guns[d->gunselect].range; float 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(guns[d->gunselect].rays > 1) createrays(d->gunselect, from, to); else if(guns[d->gunselect].spread) offsetray(from, to, guns[d->gunselect].spread, guns[d->gunselect].range, to); hits.setsize(0); if(!guns[d->gunselect].projspeed) raydamage(from, to, d); shoteffects(d->gunselect, from, to, d, true, 0, prevaction); if(d==player1 || d->ai) { addmsg(N_SHOOT, "rci2i6iv", d, lastmillis-maptime, d->gunselect, (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 = guns[d->gunselect].attackdelay; if(d->gunselect == GUN_PISTOL && d->ai) d->gunwait += int(d->gunwait*(((101-d->skill)+rnd(111-d->skill))/100.f)); d->totalshots += guns[d->gunselect].damage*(d->quadmillis ? 4 : 1)*guns[d->gunselect].rays; } void adddynlights() { loopv(projs) { projectile &p = projs[i]; if(p.gun!=GUN_RL) continue; vec pos(p.o); pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS))); adddynlight(pos, 20, vec(1, 0.75f, 0.5f)); } loopv(bouncers) { bouncer &bnc = *bouncers[i]; if(bnc.bouncetype!=BNC_GRENADE) continue; vec pos = bnc.offsetpos(); adddynlight(pos, 8, vec(0.25f, 1, 1)); } } static const char * const projnames[2] = { "projectiles/grenade", "projectiles/rocket" }; void preloadbouncers() { loopi(sizeof(projnames)/sizeof(projnames[0])) preloadmodel(projnames[i]); } void renderbouncers() { float yaw, pitch; loopv(bouncers) { bouncer &bnc = *bouncers[i]; vec pos = bnc.offsetpos(); 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; if(bnc.bouncetype==BNC_GRENADE) rendermodel(&bnc.light, "projectiles/grenade", ANIM_MAPMODEL|ANIM_LOOP, pos, yaw, pitch, MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHT_FAST|MDL_DYNSHADOW); else { 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; rendermodel(&bnc.light, mdl, ANIM_MAPMODEL|ANIM_LOOP, pos, yaw, pitch, cull, NULL, NULL, 0, 0, fade); } } } void renderprojectiles() { float yaw, pitch; loopv(projs) { projectile &p = projs[i]; if(p.gun!=GUN_RL) continue; float dist = min(p.o.dist(p.to)/32.0f, 1.0f); vec pos = vec(p.o).add(vec(p.offset).mul(dist*p.offsetmillis/float(OFFSETMILLIS))), v = dist < 1e-6f ? p.dir : vec(p.to).sub(pos).normalize(); // the amount of distance in front of the smoke trail needs to change if the model does vectoyawpitch(v, yaw, pitch); yaw += 90; v.mul(3); v.add(pos); rendermodel(&p.light, "projectiles/rocket", ANIM_MAPMODEL|ANIM_LOOP, v, yaw, pitch, MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHT_FAST); } } void checkattacksound(fpsent *d, bool local) { int gun = -1; switch(d->attacksound) { case S_CHAINSAW_ATTACK: if(chainsawhudgun) gun = GUN_FIST; break; default: return; } if(gun >= 0 && gun < NUMGUNS && d->clientnum >= 0 && d->state == CS_ALIVE && d->lastattackgun == gun && lastmillis - d->lastaction < guns[gun].attackdelay + 50) { d->attackchan = playsound(d->attacksound, local ? NULL : &d->o, NULL, 0, -1, -1, d->attackchan); if(d->attackchan < 0) d->attacksound = -1; } else d->stopattacksound(); } void checkidlesound(fpsent *d, bool local) { int sound = -1, radius = 0; if(d->clientnum >= 0 && d->state == CS_ALIVE) switch(d->gunselect) { case GUN_FIST: if(chainsawhudgun && d->attacksound < 0) { sound = S_CHAINSAW_IDLE; radius = 50; } break; } if(d->idlesound != sound) { if(d->idlesound >= 0) d->stopidlesound(); if(sound >= 0) { d->idlechan = playsound(sound, local ? NULL : &d->o, NULL, 0, -1, 100, d->idlechan, radius); if(d->idlechan >= 0) d->idlesound = sound; } } else if(sound >= 0) { d->idlechan = playsound(sound, local ? NULL : &d->o, NULL, 0, -1, -1, d->idlechan, radius); if(d->idlechan < 0) d->idlesound = -1; } } void removeweapons(fpsent *d) { removebouncers(d); removeprojectiles(d); } void updateweapons(int curtime) { updateprojectiles(curtime); pwcalcaccuracy(); 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 grenades don't end up inside player's BB next frame fpsent *following = followingplayer(); if(!following) following = player1; loopv(players) { fpsent *d = players[i]; checkattacksound(d, d==following); checkidlesound(d, d==following); } } void avoidweapons(ai::avoidset &obstacles, float radius) { loopv(projs) { projectile &p = projs[i]; obstacles.avoidnear(NULL, p.o.z + guns[p.gun].exprad + 1, p.o, radius + guns[p.gun].exprad); } loopv(bouncers) { bouncer &bnc = *bouncers[i]; if(bnc.bouncetype != BNC_GRENADE) continue; obstacles.avoidnear(NULL, bnc.o.z + guns[GUN_GL].exprad + 1, bnc.o, radius + guns[GUN_GL].exprad); } } }; /// Rough accuracy code, client-side only. int pwshotsfired [NUMGUNS] = { 0 }; int pwshotshit [NUMGUNS] = { 0 }; int pwdamagedealt [NUMGUNS] = { 0 }; int pwaccuracy [NUMGUNS] = { 0 }; int pwavgaccuracy = 0; void pwshot(int gun) { pwshotsfired[gun]++; } void pwhit(int gun, int damage) { pwshotshit[gun]++; pwdamagedealt[gun] += damage; } void pwcalcaccuracy(void) { pwavgaccuracy = 0; loopi(NUMGUNS) if(pwshotsfired[i]) pwaccuracy[i] = (pwdamagedealt[i] * 100) / (guns[i].damage * guns[i].rays * pwshotsfired[i]); else pwaccuracy[i] = 0; loopi(NUMGUNS) pwavgaccuracy += pwaccuracy[i]; pwavgaccuracy /= NUMGUNS; } void pwreset(void) { loopi(NUMGUNS) pwshotsfired[i] = pwshotshit[i] = pwdamagedealt[i] = pwaccuracy[i] = 0; loopi(7) pwitemspicked[i] = 0; pwavgaccuracy = 0; }