--- /dev/null
- if(IsDifferentTeam(head, projectile.realowner))
- ++hit_enemy;
- else
+#ifdef REGISTER_WEAPON
+REGISTER_WEAPON(
+/* WEP_##id */ CRYLINK,
+/* function */ w_crylink,
+/* ammotype */ IT_CELLS,
+/* impulse */ 6,
+/* flags */ WEP_FLAG_NORMAL | WEP_FLAG_RELOADABLE | WEP_TYPE_SPLASH,
+/* rating */ BOT_PICKUP_RATING_MID,
+/* model */ "crylink",
+/* netname */ "crylink",
+/* fullname */ _("Crylink")
+);
+
+#define CRYLINK_SETTINGS(weapon) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, ammo) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, animtime) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, damage) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, edgedamage) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, radius) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, force) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, spread) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, refire) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, speed) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, shots) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, bounces) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, bouncedamagefactor) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, middle_lifetime) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, middle_fadetime) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, other_lifetime) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, other_fadetime) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, linkexplode) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, joindelay) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, joinspread) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, joinexplode) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, joinexplode_damage) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, joinexplode_edgedamage) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, joinexplode_radius) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, joinexplode_force) \
+ WEP_ADD_CVAR(weapon, MO_SEC, spreadtype) \
+ WEP_ADD_PROP(weapon, reloading_ammo, reload_ammo) \
+ WEP_ADD_PROP(weapon, reloading_time, reload_time) \
+ WEP_ADD_PROP(weapon, switchdelay_raise, switchdelay_raise) \
+ WEP_ADD_PROP(weapon, switchdelay_drop, switchdelay_drop)
+
+#ifdef SVQC
+CRYLINK_SETTINGS(crylink)
+.float gravity;
+.float crylink_waitrelease;
+.entity crylink_lastgroup;
+
+.entity queuenext;
+.entity queueprev;
+#endif
+#else
+#ifdef SVQC
+void spawnfunc_weapon_crylink() { weapon_defaultspawnfunc(WEP_CRYLINK); }
+
+void W_Crylink_CheckLinks(entity e)
+{
+ float i;
+ entity p;
+
+ if(e == world)
+ error("W_Crylink_CheckLinks: entity is world");
+ if(e.classname != "spike" || wasfreed(e))
+ error(sprintf("W_Crylink_CheckLinks: entity is not a spike but a %s (freed: %d)", e.classname, wasfreed(e)));
+
+ p = e;
+ for(i = 0; i < 1000; ++i)
+ {
+ if(p.queuenext.queueprev != p || p.queueprev.queuenext != p)
+ error("W_Crylink_CheckLinks: queue is inconsistent");
+ p = p.queuenext;
+ if(p == e)
+ break;
+ }
+ if(i >= 1000)
+ error("W_Crylink_CheckLinks: infinite chain");
+}
+
+void W_Crylink_Dequeue_Raw(entity own, entity prev, entity me, entity next)
+{
+ W_Crylink_CheckLinks(next);
+ if(me == own.crylink_lastgroup)
+ own.crylink_lastgroup = ((me == next) ? world : next);
+ prev.queuenext = next;
+ next.queueprev = prev;
+ me.classname = "spike_oktoremove";
+ if(me != next)
+ W_Crylink_CheckLinks(next);
+}
+
+void W_Crylink_Dequeue(entity e)
+{
+ W_Crylink_Dequeue_Raw(e.realowner, e.queueprev, e, e.queuenext);
+}
+
+void W_Crylink_Reset(void)
+{
+ W_Crylink_Dequeue(self);
+ remove(self);
+}
+
+// force projectile to explode
+void W_Crylink_LinkExplode (entity e, entity e2)
+{
+ float a;
+
+ if(e == e2)
+ return;
+
+ a = bound(0, 1 - (time - e.fade_time) * e.fade_rate, 1);
+
+ if(e == e.realowner.crylink_lastgroup)
+ e.realowner.crylink_lastgroup = world;
+
+ float isprimary = !(e.projectiledeathtype & HITTYPE_SECONDARY);
+
+ RadiusDamage(e, e.realowner, WEP_CVAR_BOTH(crylink, isprimary, damage) * a, WEP_CVAR_BOTH(crylink, isprimary, edgedamage) * a, WEP_CVAR_BOTH(crylink, isprimary, radius), world, world, WEP_CVAR_BOTH(crylink, isprimary, force) * a, e.projectiledeathtype, other);
+
+ W_Crylink_LinkExplode(e.queuenext, e2);
+
+ e.classname = "spike_oktoremove";
+ remove (e);
+}
+
+// adjust towards center
+// returns the origin where they will meet... and the time till the meeting is
+// stored in w_crylink_linkjoin_time.
+// could possibly network this origin and time, and display a special particle
+// effect when projectiles meet there :P
+// jspeed: joining speed (calculate this as join spread * initial speed)
+float w_crylink_linkjoin_time;
+vector W_Crylink_LinkJoin(entity e, float jspeed)
+{
+ vector avg_origin, avg_velocity;
+ vector targ_origin;
+ float avg_dist, n;
+ entity p;
+
+ // FIXME remove this debug code
+ W_Crylink_CheckLinks(e);
+
+ w_crylink_linkjoin_time = 0;
+
+ avg_origin = e.origin;
+ avg_velocity = e.velocity;
+ n = 1;
+ for(p = e; (p = p.queuenext) != e; )
+ {
+ avg_origin += WarpZone_RefSys_TransformOrigin(p, e, p.origin);
+ avg_velocity += WarpZone_RefSys_TransformVelocity(p, e, p.velocity);
+ ++n;
+ }
+ avg_origin *= (1.0 / n);
+ avg_velocity *= (1.0 / n);
+
+ if(n < 2)
+ return avg_origin; // nothing to do
+
+ // yes, mathematically we can do this in ONE step, but beware of 32bit floats...
+ avg_dist = pow(vlen(e.origin - avg_origin), 2);
+ for(p = e; (p = p.queuenext) != e; )
+ avg_dist += pow(vlen(WarpZone_RefSys_TransformOrigin(p, e, p.origin) - avg_origin), 2);
+ avg_dist *= (1.0 / n);
+ avg_dist = sqrt(avg_dist);
+
+ if(avg_dist == 0)
+ return avg_origin; // no change needed
+
+ if(jspeed == 0)
+ {
+ e.velocity = avg_velocity;
+ UpdateCSQCProjectile(e);
+ for(p = e; (p = p.queuenext) != e; )
+ {
+ p.velocity = WarpZone_RefSys_TransformVelocity(e, p, avg_velocity);
+ UpdateCSQCProjectile(p);
+ }
+ targ_origin = avg_origin + 1000000000 * normalize(avg_velocity); // HUUUUUUGE
+ }
+ else
+ {
+ w_crylink_linkjoin_time = avg_dist / jspeed;
+ targ_origin = avg_origin + w_crylink_linkjoin_time * avg_velocity;
+
+ e.velocity = (targ_origin - e.origin) * (1.0 / w_crylink_linkjoin_time);
+ UpdateCSQCProjectile(e);
+ for(p = e; (p = p.queuenext) != e; )
+ {
+ p.velocity = WarpZone_RefSys_TransformVelocity(e, p, (targ_origin - WarpZone_RefSys_TransformOrigin(p, e, p.origin)) * (1.0 / w_crylink_linkjoin_time));
+ UpdateCSQCProjectile(p);
+ }
+
+ // analysis:
+ // jspeed -> +infinity:
+ // w_crylink_linkjoin_time -> +0
+ // targ_origin -> avg_origin
+ // p->velocity -> HUEG towards center
+ // jspeed -> 0:
+ // w_crylink_linkjoin_time -> +/- infinity
+ // targ_origin -> avg_velocity * +/- infinity
+ // p->velocity -> avg_velocity
+ // jspeed -> -infinity:
+ // w_crylink_linkjoin_time -> -0
+ // targ_origin -> avg_origin
+ // p->velocity -> HUEG away from center
+ }
+
+ W_Crylink_CheckLinks(e);
+
+ return targ_origin;
+}
+
+void W_Crylink_LinkJoinEffect_Think()
+{
+ // is there at least 2 projectiles very close?
+ entity e, p;
+ float n;
+ e = self.owner.crylink_lastgroup;
+ n = 0;
+ if(e)
+ {
+ if(vlen(e.origin - self.origin) < vlen(e.velocity) * frametime)
+ ++n;
+ for(p = e; (p = p.queuenext) != e; )
+ {
+ if(vlen(p.origin - self.origin) < vlen(p.velocity) * frametime)
+ ++n;
+ }
+ if(n >= 2)
+ {
+ float isprimary = !(e.projectiledeathtype & HITTYPE_SECONDARY);
+
+ if(WEP_CVAR_BOTH(crylink, isprimary, joinexplode))
+ {
+ n /= WEP_CVAR_BOTH(crylink, isprimary, shots);
+ RadiusDamage(e, e.realowner, WEP_CVAR_BOTH(crylink, isprimary, joinexplode_damage) * n,
+ WEP_CVAR_BOTH(crylink, isprimary, joinexplode_edgedamage) * n,
+ WEP_CVAR_BOTH(crylink, isprimary, joinexplode_radius) * n, e.realowner, world,
+ WEP_CVAR_BOTH(crylink, isprimary, joinexplode_force) * n, e.projectiledeathtype, other);
+ pointparticles(particleeffectnum("crylink_joinexplode"), self.origin, '0 0 0', n);
+ }
+ }
+ }
+ remove(self);
+}
+
+float W_Crylink_Touch_WouldHitFriendly(entity projectile, float rad)
+{
+ entity head = WarpZone_FindRadius((projectile.origin + (projectile.mins + projectile.maxs) * 0.5), rad + MAX_DAMAGEEXTRARADIUS, FALSE);
+ float hit_friendly = 0;
+ float hit_enemy = 0;
+
+ while(head)
+ {
+ if((head.takedamage != DAMAGE_NO) && (head.deadflag == DEAD_NO))
+ {
++ if(SAME_TEAM(head, projectile.realowner))
+ ++hit_friendly;
++ else
++ ++hit_enemy;
+ }
+
+ head = head.chain;
+ }
+
+ return (hit_enemy ? FALSE : hit_friendly);
+}
+
+// NO bounce protection, as bounces are limited!
+void W_Crylink_Touch (void)
+{
+ float finalhit;
+ float f;
+ float isprimary = !(self.projectiledeathtype & HITTYPE_SECONDARY);
+ PROJECTILE_TOUCH;
+
+ float a;
+ a = bound(0, 1 - (time - self.fade_time) * self.fade_rate, 1);
+
+ finalhit = ((self.cnt <= 0) || (other.takedamage != DAMAGE_NO));
+ if(finalhit)
+ f = 1;
+ else
+ f = WEP_CVAR_BOTH(crylink, isprimary, bouncedamagefactor);
+ if(a)
+ f *= a;
+
+ float totaldamage = RadiusDamage(self, self.realowner, WEP_CVAR_BOTH(crylink, isprimary, damage) * f, WEP_CVAR_BOTH(crylink, isprimary, edgedamage) * f, WEP_CVAR_BOTH(crylink, isprimary, radius), world, world, WEP_CVAR_BOTH(crylink, isprimary, force) * f, self.projectiledeathtype, other);
+
+ if(totaldamage && ((WEP_CVAR_BOTH(crylink, isprimary, linkexplode) == 2) || ((WEP_CVAR_BOTH(crylink, isprimary, linkexplode) == 1) && !W_Crylink_Touch_WouldHitFriendly(self, WEP_CVAR_BOTH(crylink, isprimary, radius)))))
+ {
+ if(self == self.realowner.crylink_lastgroup)
+ self.realowner.crylink_lastgroup = world;
+ W_Crylink_LinkExplode(self.queuenext, self);
+ self.classname = "spike_oktoremove";
+ remove (self);
+ return;
+ }
+ else if(finalhit)
+ {
+ // just unlink
+ W_Crylink_Dequeue(self);
+ remove(self);
+ return;
+ }
+ self.cnt = self.cnt - 1;
+ self.angles = vectoangles(self.velocity);
+ self.owner = world;
+ self.projectiledeathtype |= HITTYPE_BOUNCE;
+ // commented out as it causes a little hitch...
+ //if(proj.cnt == 0)
+ // CSQCProjectile(proj, TRUE, PROJECTILE_CRYLINK, TRUE);
+}
+
+void W_Crylink_Fadethink (void)
+{
+ W_Crylink_Dequeue(self);
+ remove(self);
+}
+
+void W_Crylink_Attack (void)
+{
+ float counter, shots;
+ entity proj, prevproj, firstproj;
+ vector s;
+ vector forward, right, up;
+ float maxdmg;
+
+ W_DecreaseAmmo(ammo_cells, autocvar_g_balance_crylink_primary_ammo, autocvar_g_balance_crylink_reload_ammo);
+
+ maxdmg = WEP_CVAR_PRI(crylink, damage) * WEP_CVAR_PRI(crylink, shots);
+ maxdmg *= 1 + WEP_CVAR_PRI(crylink, bouncedamagefactor) * WEP_CVAR_PRI(crylink, bounces);
+ if(WEP_CVAR_PRI(crylink, joinexplode))
+ maxdmg += WEP_CVAR_PRI(crylink, joinexplode_damage);
+
+ W_SetupShot (self, FALSE, 2, "weapons/crylink_fire.wav", CH_WEAPON_A, maxdmg);
+ forward = v_forward;
+ right = v_right;
+ up = v_up;
+
+ shots = WEP_CVAR_PRI(crylink, shots);
+ pointparticles(particleeffectnum("crylink_muzzleflash"), w_shotorg, w_shotdir * 1000, shots);
+ proj = prevproj = firstproj = world;
+ for(counter = 0; counter < shots; ++counter)
+ {
+ proj = spawn ();
+ proj.reset = W_Crylink_Reset;
+ proj.realowner = proj.owner = self;
+ proj.classname = "spike";
+ proj.bot_dodge = TRUE;
+ proj.bot_dodgerating = WEP_CVAR_PRI(crylink, damage);
+ if(shots == 1) {
+ proj.queuenext = proj;
+ proj.queueprev = proj;
+ }
+ else if(counter == 0) { // first projectile, store in firstproj for now
+ firstproj = proj;
+ }
+ else if(counter == shots - 1) { // last projectile, link up with first projectile
+ prevproj.queuenext = proj;
+ firstproj.queueprev = proj;
+ proj.queuenext = firstproj;
+ proj.queueprev = prevproj;
+ }
+ else { // else link up with previous projectile
+ prevproj.queuenext = proj;
+ proj.queueprev = prevproj;
+ }
+
+ prevproj = proj;
+
+ proj.movetype = MOVETYPE_BOUNCEMISSILE;
+ PROJECTILE_MAKETRIGGER(proj);
+ proj.projectiledeathtype = WEP_CRYLINK;
+ //proj.gravity = 0.001;
+
+ setorigin (proj, w_shotorg);
+ setsize(proj, '0 0 0', '0 0 0');
+
+
+ s = '0 0 0';
+ if (counter == 0)
+ s = '0 0 0';
+ else
+ {
+ makevectors('0 360 0' * (0.75 + (counter - 0.5) / (shots - 1)));
+ s_y = v_forward_x;
+ s_z = v_forward_y;
+ }
+ s = s * WEP_CVAR_PRI(crylink, spread) * g_weaponspreadfactor;
+ W_SetupProjectileVelocityEx(proj, w_shotdir + right * s_y + up * s_z, v_up, WEP_CVAR_PRI(crylink, speed), 0, 0, 0, FALSE);
+ proj.touch = W_Crylink_Touch;
+
+ proj.think = W_Crylink_Fadethink;
+ if(counter == 0)
+ {
+ proj.fade_time = time + WEP_CVAR_PRI(crylink, middle_lifetime);
+ proj.fade_rate = 1 / WEP_CVAR_PRI(crylink, middle_fadetime);
+ proj.nextthink = time + WEP_CVAR_PRI(crylink, middle_lifetime) + WEP_CVAR_PRI(crylink, middle_fadetime);
+ }
+ else
+ {
+ proj.fade_time = time + WEP_CVAR_PRI(crylink, other_lifetime);
+ proj.fade_rate = 1 / WEP_CVAR_PRI(crylink, other_fadetime);
+ proj.nextthink = time + WEP_CVAR_PRI(crylink, other_lifetime) + WEP_CVAR_PRI(crylink, other_fadetime);
+ }
+ proj.teleport_time = time + WEP_CVAR_PRI(crylink, joindelay);
+ proj.cnt = WEP_CVAR_PRI(crylink, bounces);
+ //proj.scale = 1 + 1 * proj.cnt;
+
+ proj.angles = vectoangles (proj.velocity);
+
+ //proj.glow_size = 20;
+
+ proj.flags = FL_PROJECTILE;
+ proj.missile_flags = MIF_SPLASH;
+
+ CSQCProjectile(proj, TRUE, (proj.cnt ? PROJECTILE_CRYLINK_BOUNCING : PROJECTILE_CRYLINK), TRUE);
+
+ other = proj; MUTATOR_CALLHOOK(EditProjectile);
+ }
+ if(WEP_CVAR_PRI(crylink, joinspread) != 0)
+ {
+ self.crylink_lastgroup = proj;
+ W_Crylink_CheckLinks(proj);
+ self.crylink_waitrelease = 1;
+ }
+}
+
+void W_Crylink_Attack2 (void)
+{
+ float counter, shots;
+ entity proj, prevproj, firstproj;
+ vector s;
+ vector forward, right, up;
+ float maxdmg;
+
+ W_DecreaseAmmo(ammo_cells, autocvar_g_balance_crylink_secondary_ammo, autocvar_g_balance_crylink_reload_ammo);
+
+ maxdmg = WEP_CVAR_SEC(crylink, damage) * WEP_CVAR_SEC(crylink, shots);
+ maxdmg *= 1 + WEP_CVAR_SEC(crylink, bouncedamagefactor) * WEP_CVAR_SEC(crylink, bounces);
+ if(WEP_CVAR_SEC(crylink, joinexplode))
+ maxdmg += WEP_CVAR_SEC(crylink, joinexplode_damage);
+
+ W_SetupShot (self, FALSE, 2, "weapons/crylink_fire2.wav", CH_WEAPON_A, maxdmg);
+ forward = v_forward;
+ right = v_right;
+ up = v_up;
+
+ shots = WEP_CVAR_SEC(crylink, shots);
+ pointparticles(particleeffectnum("crylink_muzzleflash"), w_shotorg, w_shotdir * 1000, shots);
+ proj = prevproj = firstproj = world;
+ for(counter = 0; counter < shots; ++counter)
+ {
+ proj = spawn ();
+ proj.reset = W_Crylink_Reset;
+ proj.realowner = proj.owner = self;
+ proj.classname = "spike";
+ proj.bot_dodge = TRUE;
+ proj.bot_dodgerating = WEP_CVAR_SEC(crylink, damage);
+ if(shots == 1) {
+ proj.queuenext = proj;
+ proj.queueprev = proj;
+ }
+ else if(counter == 0) { // first projectile, store in firstproj for now
+ firstproj = proj;
+ }
+ else if(counter == shots - 1) { // last projectile, link up with first projectile
+ prevproj.queuenext = proj;
+ firstproj.queueprev = proj;
+ proj.queuenext = firstproj;
+ proj.queueprev = prevproj;
+ }
+ else { // else link up with previous projectile
+ prevproj.queuenext = proj;
+ proj.queueprev = prevproj;
+ }
+
+ prevproj = proj;
+
+ proj.movetype = MOVETYPE_BOUNCEMISSILE;
+ PROJECTILE_MAKETRIGGER(proj);
+ proj.projectiledeathtype = WEP_CRYLINK | HITTYPE_SECONDARY;
+ //proj.gravity = 0.001;
+
+ setorigin (proj, w_shotorg);
+ setsize(proj, '0 0 0', '0 0 0');
+
+ if(WEP_CVAR_SEC(crylink, spreadtype) == 1)
+ {
+ s = '0 0 0';
+ if (counter == 0)
+ s = '0 0 0';
+ else
+ {
+ makevectors('0 360 0' * (0.75 + (counter - 0.5) / (shots - 1)));
+ s_y = v_forward_x;
+ s_z = v_forward_y;
+ }
+ s = s * WEP_CVAR_SEC(crylink, spread) * g_weaponspreadfactor;
+ s = w_shotdir + right * s_y + up * s_z;
+ }
+ else
+ {
+ s = (w_shotdir + (((counter + 0.5) / shots) * 2 - 1) * v_right * WEP_CVAR_SEC(crylink, spread) * g_weaponspreadfactor);
+ }
+
+ W_SetupProjectileVelocityEx(proj, s, v_up, WEP_CVAR_SEC(crylink, speed), 0, 0, 0, FALSE);
+ proj.touch = W_Crylink_Touch;
+ proj.think = W_Crylink_Fadethink;
+ if(counter == (shots - 1) / 2)
+ {
+ proj.fade_time = time + WEP_CVAR_SEC(crylink, middle_lifetime);
+ proj.fade_rate = 1 / WEP_CVAR_SEC(crylink, middle_fadetime);
+ proj.nextthink = time + WEP_CVAR_SEC(crylink, middle_lifetime) + WEP_CVAR_SEC(crylink, middle_fadetime);
+ }
+ else
+ {
+ proj.fade_time = time + WEP_CVAR_SEC(crylink, other_lifetime);
+ proj.fade_rate = 1 / WEP_CVAR_SEC(crylink, other_fadetime);
+ proj.nextthink = time + WEP_CVAR_SEC(crylink, other_lifetime) + WEP_CVAR_SEC(crylink, other_fadetime);
+ }
+ proj.teleport_time = time + WEP_CVAR_SEC(crylink, joindelay);
+ proj.cnt = WEP_CVAR_SEC(crylink, bounces);
+ //proj.scale = 1 + 1 * proj.cnt;
+
+ proj.angles = vectoangles (proj.velocity);
+
+ //proj.glow_size = 20;
+
+ proj.flags = FL_PROJECTILE;
+ proj.missile_flags = MIF_SPLASH;
+
+ CSQCProjectile(proj, TRUE, (proj.cnt ? PROJECTILE_CRYLINK_BOUNCING : PROJECTILE_CRYLINK), TRUE);
+
+ other = proj; MUTATOR_CALLHOOK(EditProjectile);
+ }
+ if(WEP_CVAR_SEC(crylink, joinspread) != 0)
+ {
+ self.crylink_lastgroup = proj;
+ W_Crylink_CheckLinks(proj);
+ self.crylink_waitrelease = 2;
+ }
+}
+
+float w_crylink(float req)
+{
+ float ammo_amount;
+ switch(req)
+ {
+ case WR_AIM:
+ {
+ if (random() < 0.10)
+ self.BUTTON_ATCK = bot_aim(WEP_CVAR_PRI(crylink, speed), 0, WEP_CVAR_PRI(crylink, middle_lifetime), FALSE);
+ else
+ self.BUTTON_ATCK2 = bot_aim(WEP_CVAR_SEC(crylink, speed), 0, WEP_CVAR_SEC(crylink, middle_lifetime), FALSE);
+
+ return TRUE;
+ }
+ case WR_THINK:
+ {
+ if(autocvar_g_balance_crylink_reload_ammo && self.clip_load < min(autocvar_g_balance_crylink_primary_ammo, autocvar_g_balance_crylink_secondary_ammo)) // forced reload
+ WEP_ACTION(self.weapon, WR_RELOAD);
+
+ if (self.BUTTON_ATCK)
+ {
+ if (self.crylink_waitrelease != 1)
+ if (weapon_prepareattack(0, WEP_CVAR_PRI(crylink, refire)))
+ {
+ W_Crylink_Attack();
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR_PRI(crylink, animtime), w_ready);
+ }
+ }
+
+ if(self.BUTTON_ATCK2 && autocvar_g_balance_crylink_secondary)
+ {
+ if (self.crylink_waitrelease != 2)
+ if (weapon_prepareattack(1, WEP_CVAR_SEC(crylink, refire)))
+ {
+ W_Crylink_Attack2();
+ weapon_thinkf(WFRAME_FIRE2, WEP_CVAR_SEC(crylink, animtime), w_ready);
+ }
+ }
+
+ if ((self.crylink_waitrelease == 1 && !self.BUTTON_ATCK) || (self.crylink_waitrelease == 2 && !self.BUTTON_ATCK2))
+ {
+ if (!self.crylink_lastgroup || time > self.crylink_lastgroup.teleport_time)
+ {
+ // fired and released now!
+ if(self.crylink_lastgroup)
+ {
+ vector pos;
+ entity linkjoineffect;
+ float isprimary = (self.crylink_waitrelease == 1);
+
+ pos = W_Crylink_LinkJoin(self.crylink_lastgroup, WEP_CVAR_BOTH(crylink, isprimary, joinspread) * WEP_CVAR_BOTH(crylink, isprimary, speed));
+
+ linkjoineffect = spawn();
+ linkjoineffect.think = W_Crylink_LinkJoinEffect_Think;
+ linkjoineffect.classname = "linkjoineffect";
+ linkjoineffect.nextthink = time + w_crylink_linkjoin_time;
+ linkjoineffect.owner = self;
+ setorigin(linkjoineffect, pos);
+ }
+ self.crylink_waitrelease = 0;
+ if(!w_crylink(WR_CHECKAMMO1) && !w_crylink(WR_CHECKAMMO2))
+ if not(self.items & IT_UNLIMITED_WEAPON_AMMO)
+ {
+ // ran out of ammo!
+ self.cnt = WEP_CRYLINK;
+ self.switchweapon = w_getbestweapon(self);
+ }
+ }
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_model ("models/weapons/g_crylink.md3");
+ precache_model ("models/weapons/v_crylink.md3");
+ precache_model ("models/weapons/h_crylink.iqm");
+ precache_sound ("weapons/crylink_fire.wav");
+ precache_sound ("weapons/crylink_fire2.wav");
+ precache_sound ("weapons/crylink_linkjoin.wav");
+ WEP_SET_PROPS(CRYLINK_SETTINGS(crylink), WEP_CRYLINK)
+ return TRUE;
+ }
+ case WR_SETUP:
+ {
+ self.current_ammo = ammo_cells;
+ return TRUE;
+ }
+ case WR_CHECKAMMO1:
+ {
+ // don't "run out of ammo" and switch weapons while waiting for release
+ if(self.crylink_lastgroup && self.crylink_waitrelease)
+ return TRUE;
+
+ ammo_amount = self.ammo_cells >= autocvar_g_balance_crylink_primary_ammo;
+ ammo_amount += self.(weapon_load[WEP_CRYLINK]) >= autocvar_g_balance_crylink_primary_ammo;
+ return ammo_amount;
+ }
+ case WR_CHECKAMMO2:
+ {
+ // don't "run out of ammo" and switch weapons while waiting for release
+ if(self.crylink_lastgroup && self.crylink_waitrelease)
+ return TRUE;
+
+ ammo_amount = self.ammo_cells >= autocvar_g_balance_crylink_secondary_ammo;
+ ammo_amount += self.(weapon_load[WEP_CRYLINK]) >= autocvar_g_balance_crylink_secondary_ammo;
+ return ammo_amount;
+ }
+ case WR_CONFIG:
+ {
+ WEP_CONFIG_SETTINGS(CRYLINK_SETTINGS(crylink))
+ return TRUE;
+ }
+ case WR_RELOAD:
+ {
+ W_Reload(min(autocvar_g_balance_crylink_primary_ammo, autocvar_g_balance_crylink_secondary_ammo), "weapons/reload.wav");
+ return TRUE;
+ }
+ case WR_SUICIDEMESSAGE:
+ {
+ return WEAPON_CRYLINK_SUICIDE;
+ }
+ case WR_KILLMESSAGE:
+ {
+ return WEAPON_CRYLINK_MURDER;
+ }
+ }
+ return TRUE;
+}
+#endif
+#ifdef CSQC
+float w_crylink(float req)
+{
+ switch(req)
+ {
+ case WR_IMPACTEFFECT:
+ {
+ vector org2;
+ org2 = w_org + w_backoff * 2;
+ if(w_deathtype & HITTYPE_SECONDARY)
+ {
+ pointparticles(particleeffectnum("crylink_impact"), org2, '0 0 0', 1);
+ if(!w_issilent)
+ sound(self, CH_SHOTS, "weapons/crylink_impact2.wav", VOL_BASE, ATTN_NORM);
+ }
+ else
+ {
+ pointparticles(particleeffectnum("crylink_impactbig"), org2, '0 0 0', 1);
+ if(!w_issilent)
+ sound(self, CH_SHOTS, "weapons/crylink_impact.wav", VOL_BASE, ATTN_NORM);
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_sound("weapons/crylink_impact2.wav");
+ precache_sound("weapons/crylink_impact.wav");
+ return TRUE;
+ }
+ }
+ return TRUE;
+}
+#endif
+#endif
--- /dev/null
- if(IsDifferentTeam(self.realowner, other))
+#ifdef REGISTER_WEAPON
+REGISTER_WEAPON(
+/* WEP_##id */ DEVASTATOR,
+/* function */ W_Devastator,
+/* ammotype */ IT_ROCKETS,
+/* impulse */ 9,
+/* flags */ WEP_FLAG_NORMAL | WEP_FLAG_RELOADABLE | WEP_FLAG_CANCLIMB | WEP_TYPE_SPLASH,
+/* rating */ BOT_PICKUP_RATING_HIGH,
+/* model */ "rl",
+/* netname */ "devastator",
+/* fullname */ _("Devastator")
+);
+
+#define DEVASTATOR_SETTINGS(weapon) \
+ WEP_ADD_CVAR(weapon, MO_NONE, ammo) \
+ WEP_ADD_CVAR(weapon, MO_NONE, animtime) \
+ WEP_ADD_CVAR(weapon, MO_NONE, damage) \
+ WEP_ADD_CVAR(weapon, MO_NONE, damageforcescale) \
+ WEP_ADD_CVAR(weapon, MO_NONE, detonatedelay) \
+ WEP_ADD_CVAR(weapon, MO_NONE, edgedamage) \
+ WEP_ADD_CVAR(weapon, MO_NONE, force) \
+ WEP_ADD_CVAR(weapon, MO_NONE, guidedelay) \
+ WEP_ADD_CVAR(weapon, MO_NONE, guidegoal) \
+ WEP_ADD_CVAR(weapon, MO_NONE, guiderate) \
+ WEP_ADD_CVAR(weapon, MO_NONE, guideratedelay) \
+ WEP_ADD_CVAR(weapon, MO_NONE, guidestop) \
+ WEP_ADD_CVAR(weapon, MO_NONE, health) \
+ WEP_ADD_CVAR(weapon, MO_NONE, lifetime) \
+ WEP_ADD_CVAR(weapon, MO_NONE, radius) \
+ WEP_ADD_CVAR(weapon, MO_NONE, refire) \
+ WEP_ADD_CVAR(weapon, MO_NONE, remote_damage) \
+ WEP_ADD_CVAR(weapon, MO_NONE, remote_edgedamage) \
+ WEP_ADD_CVAR(weapon, MO_NONE, remote_force) \
+ WEP_ADD_CVAR(weapon, MO_NONE, remote_radius) \
+ WEP_ADD_CVAR(weapon, MO_NONE, speed) \
+ WEP_ADD_CVAR(weapon, MO_NONE, speedaccel) \
+ WEP_ADD_CVAR(weapon, MO_NONE, speedstart) \
+ WEP_ADD_PROP(weapon, reloading_ammo, reload_ammo) \
+ WEP_ADD_PROP(weapon, reloading_time, reload_time) \
+ WEP_ADD_PROP(weapon, switchdelay_raise, switchdelay_raise) \
+ WEP_ADD_PROP(weapon, switchdelay_drop, switchdelay_drop)
+
+#ifdef SVQC
+DEVASTATOR_SETTINGS(devastator)
+.float rl_release;
+.float rl_detonate_later;
+#endif
+#else
+#ifdef SVQC
+void spawnfunc_weapon_devastator() { weapon_defaultspawnfunc(WEP_DEVASTATOR); }
+void spawnfunc_weapon_rocketlauncher() { spawnfunc_weapon_devastator(); }
+
+void W_Devastator_Unregister()
+{
+ if(self.realowner && self.realowner.lastrocket == self)
+ {
+ self.realowner.lastrocket = world;
+ // self.realowner.rl_release = 1;
+ }
+}
+
+void W_Devastator_Explode()
+{
+ W_Devastator_Unregister();
+
+ if(other.takedamage == DAMAGE_AIM)
+ if(IS_PLAYER(other))
++ if(DIFF_TEAM(self.realowner, other))
+ if(other.deadflag == DEAD_NO)
+ if(IsFlying(other))
+ Send_Notification(NOTIF_ONE, self.realowner, MSG_ANNCE, ANNCE_ACHIEVEMENT_AIRSHOT);
+
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+
+ RadiusDamage (self, self.realowner, WEP_CVAR(devastator, damage), WEP_CVAR(devastator, edgedamage), WEP_CVAR(devastator, radius), world, world, WEP_CVAR(devastator, force), self.projectiledeathtype, other);
+
+ if (self.realowner.weapon == WEP_DEVASTATOR)
+ {
+ if(self.realowner.ammo_rockets < WEP_CVAR(devastator, ammo))
+ {
+ self.realowner.cnt = WEP_DEVASTATOR;
+ ATTACK_FINISHED(self.realowner) = time;
+ self.realowner.switchweapon = w_getbestweapon(self.realowner);
+ }
+ }
+ remove (self);
+}
+
+void W_Devastator_DoRemoteExplode()
+{
+ W_Devastator_Unregister();
+
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+
+ RadiusDamage (self, self.realowner, WEP_CVAR(devastator, remote_damage), WEP_CVAR(devastator, remote_edgedamage), WEP_CVAR(devastator, remote_radius), world, world, WEP_CVAR(devastator, remote_force), self.projectiledeathtype | HITTYPE_BOUNCE, world);
+
+ if (self.realowner.weapon == WEP_DEVASTATOR)
+ {
+ if(self.realowner.ammo_rockets < WEP_CVAR(devastator, ammo))
+ {
+ self.realowner.cnt = WEP_DEVASTATOR;
+ ATTACK_FINISHED(self.realowner) = time;
+ self.realowner.switchweapon = w_getbestweapon(self.realowner);
+ }
+ }
+ remove (self);
+}
+
+void W_Devastator_RemoteExplode()
+{
+ if(self.realowner.deadflag == DEAD_NO)
+ if(self.realowner.lastrocket)
+ {
+ if((self.spawnshieldtime >= 0)
+ ? (time >= self.spawnshieldtime) // timer
+ : (vlen(NearestPointOnBox(self.realowner, self.origin) - self.origin) > WEP_CVAR(devastator, remote_radius)) // safety device
+ )
+ {
+ W_Devastator_DoRemoteExplode();
+ }
+ }
+}
+
+vector W_Devastator_SteerTo(vector thisdir, vector goaldir, float maxturn_cos)
+{
+ if(thisdir * goaldir > maxturn_cos)
+ return goaldir;
+ if(thisdir * goaldir < -0.9998) // less than 1 degree and opposite
+ return thisdir; // refuse to guide (better than letting a numerical error happen)
+ float f, m2;
+ vector v;
+ // solve:
+ // g = normalize(thisdir + goaldir * X)
+ // thisdir * g = maxturn
+ //
+ // gg = thisdir + goaldir * X
+ // (thisdir * gg)^2 = maxturn^2 * (gg * gg)
+ //
+ // (1 + (thisdir * goaldir) * X)^2 = maxturn^2 * (1 + X*X + 2 * X * thisdir * goaldir)
+ f = thisdir * goaldir;
+ // (1 + f * X)^2 = maxturn^2 * (1 + X*X + 2 * X * f)
+ // 0 = (m^2 - f^2) * x^2 + (2 * f * (m^2 - 1)) * x + (m^2 - 1)
+ m2 = maxturn_cos * maxturn_cos;
+ v = solve_quadratic(m2 - f * f, 2 * f * (m2 - 1), m2 - 1);
+ return normalize(thisdir + goaldir * v_y); // the larger solution!
+}
+// assume thisdir == -goaldir:
+// f == -1
+// v = solve_qadratic(m2 - 1, -2 * (m2 - 1), m2 - 1)
+// (m2 - 1) x^2 - 2 * (m2 - 1) * x + (m2 - 1) = 0
+// x^2 - 2 * x + 1 = 0
+// (x - 1)^2 = 0
+// x = 1
+// normalize(thisdir + goaldir)
+// normalize(0)
+
+void W_Devastator_Think (void)
+{
+ vector desireddir, olddir, newdir, desiredorigin, goal;
+#if 0
+ float cosminang, cosmaxang, cosang;
+#endif
+ float velspeed, f;
+ self.nextthink = time;
+ if (time > self.cnt)
+ {
+ other = world;
+ self.projectiledeathtype |= HITTYPE_BOUNCE;
+ W_Devastator_Explode ();
+ return;
+ }
+
+ // accelerate
+ makevectors(self.angles_x * '-1 0 0' + self.angles_y * '0 1 0');
+ velspeed = WEP_CVAR(devastator, speed) * g_weaponspeedfactor - (self.velocity * v_forward);
+ if (velspeed > 0)
+ self.velocity = self.velocity + v_forward * min(WEP_CVAR(devastator, speedaccel) * g_weaponspeedfactor * frametime, velspeed);
+
+ // laser guided, or remote detonation
+ if (self.realowner.weapon == WEP_DEVASTATOR)
+ {
+ if(self == self.realowner.lastrocket)
+ if not(self.realowner.rl_release)
+ if not(self.BUTTON_ATCK2)
+ if(WEP_CVAR(devastator, guiderate))
+ if(time > self.pushltime)
+ if(self.realowner.deadflag == DEAD_NO)
+ {
+ f = WEP_CVAR(devastator, guideratedelay);
+ if(f)
+ f = bound(0, (time - self.pushltime) / f, 1);
+ else
+ f = 1;
+
+ velspeed = vlen(self.velocity);
+
+ makevectors(self.realowner.v_angle);
+ desireddir = WarpZone_RefSys_TransformVelocity(self.realowner, self, v_forward);
+ desiredorigin = WarpZone_RefSys_TransformOrigin(self.realowner, self, self.realowner.origin + self.realowner.view_ofs);
+ olddir = normalize(self.velocity);
+
+ // now it gets tricky... we want to move like some curve to approximate the target direction
+ // but we are limiting the rate at which we can turn!
+ goal = desiredorigin + ((self.origin - desiredorigin) * desireddir + WEP_CVAR(devastator, guidegoal)) * desireddir;
+ newdir = W_Devastator_SteerTo(olddir, normalize(goal - self.origin), cos(WEP_CVAR(devastator, guiderate) * f * frametime * DEG2RAD));
+
+ self.velocity = newdir * velspeed;
+ self.angles = vectoangles(self.velocity);
+
+ if(!self.count)
+ {
+ pointparticles(particleeffectnum("rocket_guide"), self.origin, self.velocity, 1);
+ // TODO add a better sound here
+ sound (self.realowner, CH_WEAPON_B, "weapons/rocket_mode.wav", VOL_BASE, ATTN_NORM);
+ self.count = 1;
+ }
+ }
+
+ if(self.rl_detonate_later)
+ W_Devastator_RemoteExplode();
+ }
+
+ if(self.csqcprojectile_clientanimate == 0)
+ UpdateCSQCProjectile(self);
+}
+
+void W_Devastator_Touch (void)
+{
+ if(WarpZone_Projectile_Touch())
+ {
+ if(wasfreed(self))
+ W_Devastator_Unregister();
+ return;
+ }
+ W_Devastator_Unregister();
+ W_Devastator_Explode ();
+}
+
+void W_Devastator_Damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+{
+ if (self.health <= 0)
+ return;
+
+ if (!W_CheckProjectileDamage(inflictor.realowner, self.realowner, deathtype, -1)) // no exceptions
+ return; // g_projectiles_damage says to halt
+
+ self.health = self.health - damage;
+ self.angles = vectoangles(self.velocity);
+
+ if (self.health <= 0)
+ W_PrepareExplosionByDamage(attacker, W_Devastator_Explode);
+}
+
+void W_Devastator_Attack (void)
+{
+ entity missile;
+ entity flash;
+
+ W_DecreaseAmmo(ammo_rockets, WEP_CVAR(devastator, ammo), WEP_CVAR(devastator, reload_ammo));
+
+ W_SetupShot_ProjectileSize (self, '-3 -3 -3', '3 3 3', FALSE, 5, "weapons/rocket_fire.wav", CH_WEAPON_A, WEP_CVAR(devastator, damage));
+ pointparticles(particleeffectnum("rocketlauncher_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+ missile = WarpZone_RefSys_SpawnSameRefSys(self);
+ missile.owner = missile.realowner = self;
+ self.lastrocket = missile;
+ if(WEP_CVAR(devastator, detonatedelay) >= 0)
+ missile.spawnshieldtime = time + WEP_CVAR(devastator, detonatedelay);
+ else
+ missile.spawnshieldtime = -1;
+ missile.pushltime = time + WEP_CVAR(devastator, guidedelay);
+ missile.classname = "rocket";
+ missile.bot_dodge = TRUE;
+ missile.bot_dodgerating = WEP_CVAR(devastator, damage) * 2; // * 2 because it can be detonated inflight which makes it even more dangerous
+
+ missile.takedamage = DAMAGE_YES;
+ missile.damageforcescale = WEP_CVAR(devastator, damageforcescale);
+ missile.health = WEP_CVAR(devastator, health);
+ missile.event_damage = W_Devastator_Damage;
+ missile.damagedbycontents = TRUE;
+
+ missile.movetype = MOVETYPE_FLY;
+ PROJECTILE_MAKETRIGGER(missile);
+ missile.projectiledeathtype = WEP_DEVASTATOR;
+ setsize (missile, '-3 -3 -3', '3 3 3'); // give it some size so it can be shot
+
+ setorigin (missile, w_shotorg - v_forward * 3); // move it back so it hits the wall at the right point
+ W_SetupProjectileVelocity(missile, WEP_CVAR(devastator, speedstart), 0);
+ missile.angles = vectoangles (missile.velocity);
+
+ missile.touch = W_Devastator_Touch;
+ missile.think = W_Devastator_Think;
+ missile.nextthink = time;
+ missile.cnt = time + WEP_CVAR(devastator, lifetime);
+ missile.flags = FL_PROJECTILE;
+ missile.missile_flags = MIF_SPLASH;
+
+ CSQCProjectile(missile, WEP_CVAR(devastator, guiderate) == 0 && WEP_CVAR(devastator, speedaccel) == 0, PROJECTILE_ROCKET, FALSE); // because of fly sound
+
+ // muzzle flash for 1st person view
+ flash = spawn ();
+ setmodel (flash, "models/flash.md3"); // precision set below
+ SUB_SetFade (flash, time, 0.1);
+ flash.effects = EF_ADDITIVE | EF_FULLBRIGHT | EF_LOWPRECISION;
+ W_AttachToShotorg(flash, '5 0 0');
+
+ // common properties
+ other = missile; MUTATOR_CALLHOOK(EditProjectile);
+}
+
+float W_Devastator(float req)
+{
+ entity rock;
+ float rockfound;
+ float ammo_amount;
+ switch(req)
+ {
+ case WR_AIM: // WEAPONTODO: rewrite this, it's WAY too complicated for what it should be
+ {
+ // aim and decide to fire if appropriate
+ self.BUTTON_ATCK = bot_aim(WEP_CVAR(devastator, speed), 0, WEP_CVAR(devastator, lifetime), FALSE);
+ if(skill >= 2) // skill 0 and 1 bots won't detonate rockets!
+ {
+ // decide whether to detonate rockets
+ entity missile, targetlist, targ;
+ float edgedamage, coredamage, edgeradius, recipricoledgeradius, d;
+ float selfdamage, teamdamage, enemydamage;
+ edgedamage = WEP_CVAR(devastator, edgedamage);
+ coredamage = WEP_CVAR(devastator, damage);
+ edgeradius = WEP_CVAR(devastator, radius);
+ recipricoledgeradius = 1 / edgeradius;
+ selfdamage = 0;
+ teamdamage = 0;
+ enemydamage = 0;
+ targetlist = findchainfloat(bot_attack, TRUE);
+ missile = find(world, classname, "rocket");
+ while (missile)
+ {
+ if (missile.realowner != self)
+ {
+ missile = find(missile, classname, "rocket");
+ continue;
+ }
+ targ = targetlist;
+ while (targ)
+ {
+ d = vlen(targ.origin + (targ.mins + targ.maxs) * 0.5 - missile.origin);
+ d = bound(0, edgedamage + (coredamage - edgedamage) * sqrt(1 - d * recipricoledgeradius), 10000);
+ // count potential damage according to type of target
+ if (targ == self)
+ selfdamage = selfdamage + d;
+ else if (targ.team == self.team && teamplay)
+ teamdamage = teamdamage + d;
+ else if (bot_shouldattack(targ))
+ enemydamage = enemydamage + d;
+ targ = targ.chain;
+ }
+ missile = find(missile, classname, "rocket");
+ }
+ float desirabledamage;
+ desirabledamage = enemydamage;
+ if (time > self.invincible_finished && time > self.spawnshieldtime)
+ desirabledamage = desirabledamage - selfdamage * autocvar_g_balance_selfdamagepercent;
+ if (teamplay && self.team)
+ desirabledamage = desirabledamage - teamdamage;
+
+ missile = find(world, classname, "rocket");
+ while (missile)
+ {
+ if (missile.realowner != self)
+ {
+ missile = find(missile, classname, "rocket");
+ continue;
+ }
+ makevectors(missile.v_angle);
+ targ = targetlist;
+ if (skill > 9) // normal players only do this for the target they are tracking
+ {
+ targ = targetlist;
+ while (targ)
+ {
+ if (
+ (v_forward * normalize(missile.origin - targ.origin)< 0.1)
+ && desirabledamage > 0.1*coredamage
+ )self.BUTTON_ATCK2 = TRUE;
+ targ = targ.chain;
+ }
+ }else{
+ float distance; distance= bound(300,vlen(self.origin-self.enemy.origin),30000);
+ //As the distance gets larger, a correct detonation gets near imposible
+ //Bots are assumed to use the rocket spawnfunc_light to see if the rocket gets near a player
+ if(v_forward * normalize(missile.origin - self.enemy.origin)< 0.1)
+ if(IS_PLAYER(self.enemy))
+ if(desirabledamage >= 0.1*coredamage)
+ if(random()/distance*300 > frametime*bound(0,(10-skill)*0.2,1))
+ self.BUTTON_ATCK2 = TRUE;
+ // dprint(ftos(random()/distance*300),">");dprint(ftos(frametime*bound(0,(10-skill)*0.2,1)),"\n");
+ }
+
+ missile = find(missile, classname, "rocket");
+ }
+ // if we would be doing at X percent of the core damage, detonate it
+ // but don't fire a new shot at the same time!
+ if (desirabledamage >= 0.75 * coredamage) //this should do group damage in rare fortunate events
+ self.BUTTON_ATCK2 = TRUE;
+ if ((skill > 6.5) && (selfdamage > self.health))
+ self.BUTTON_ATCK2 = FALSE;
+ //if(self.BUTTON_ATCK2 == TRUE)
+ // dprint(ftos(desirabledamage),"\n");
+ if (self.BUTTON_ATCK2 == TRUE) self.BUTTON_ATCK = FALSE;
+ }
+
+ return TRUE;
+ }
+ case WR_THINK:
+ {
+ if(WEP_CVAR(devastator, reload_ammo) && self.clip_load < WEP_CVAR(devastator, ammo)) // forced reload
+ WEP_ACTION(self.weapon, WR_RELOAD);
+ else
+ {
+ if (self.BUTTON_ATCK)
+ {
+ if(self.rl_release || WEP_CVAR(devastator, guidestop))
+ if(weapon_prepareattack(0, WEP_CVAR(devastator, refire)))
+ {
+ W_Devastator_Attack();
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR(devastator, animtime), w_ready);
+ self.rl_release = 0;
+ }
+ }
+ else
+ self.rl_release = 1;
+
+ if (self.BUTTON_ATCK2)
+ {
+ rockfound = 0;
+ for(rock = world; (rock = find(rock, classname, "rocket")); ) if(rock.realowner == self)
+ {
+ if(!rock.rl_detonate_later)
+ {
+ rock.rl_detonate_later = TRUE;
+ rockfound = 1;
+ }
+ }
+ if(rockfound)
+ sound (self, CH_WEAPON_B, "weapons/rocket_det.wav", VOL_BASE, ATTN_NORM);
+ }
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ if(autocvar_sv_precacheweapons)
+ {
+ precache_model("models/flash.md3");
+ precache_model("models/weapons/g_rl.md3");
+ precache_model("models/weapons/v_rl.md3");
+ precache_model("models/weapons/h_rl.iqm");
+ precache_sound("weapons/rocket_det.wav");
+ precache_sound("weapons/rocket_fire.wav");
+ precache_sound("weapons/rocket_mode.wav");
+ }
+ WEP_SET_PROPS(DEVASTATOR_SETTINGS(devastator), WEP_DEVASTATOR)
+ return TRUE;
+ }
+ case WR_SETUP:
+ {
+ self.current_ammo = ammo_rockets;
+ self.rl_release = 1;
+ return TRUE;
+ }
+ case WR_CHECKAMMO1:
+ {
+ // don't switch while guiding a missile
+ if (ATTACK_FINISHED(self) <= time || self.weapon != WEP_DEVASTATOR)
+ {
+ ammo_amount = FALSE;
+ if(WEP_CVAR(devastator, reload_ammo))
+ {
+ if(self.ammo_rockets < WEP_CVAR(devastator, ammo) && self.(weapon_load[WEP_DEVASTATOR]) < WEP_CVAR(devastator, ammo))
+ ammo_amount = TRUE;
+ }
+ else if(self.ammo_rockets < WEP_CVAR(devastator, ammo))
+ ammo_amount = TRUE;
+ return !ammo_amount;
+ }
+
+ return TRUE;
+ }
+ case WR_CHECKAMMO2:
+ {
+ return FALSE;
+ }
+ case WR_CONFIG:
+ {
+ WEP_CONFIG_SETTINGS(DEVASTATOR_SETTINGS(devastator))
+ return TRUE;
+ }
+ case WR_RESETPLAYER:
+ {
+ self.rl_release = 0;
+ return TRUE;
+ }
+ case WR_RELOAD:
+ {
+ W_Reload(WEP_CVAR(devastator, ammo), "weapons/reload.wav");
+ return TRUE;
+ }
+ case WR_SUICIDEMESSAGE:
+ {
+ return WEAPON_ROCKETLAUNCHER_SUICIDE;
+ }
+ case WR_KILLMESSAGE:
+ {
+ if((w_deathtype & HITTYPE_BOUNCE) || (w_deathtype & HITTYPE_SPLASH))
+ return WEAPON_ROCKETLAUNCHER_MURDER_SPLASH;
+ else
+ return WEAPON_ROCKETLAUNCHER_MURDER_DIRECT;
+ }
+ }
+ return TRUE;
+}
+#endif
+#ifdef CSQC
+float W_Devastator(float req)
+{
+ switch(req)
+ {
+ case WR_IMPACTEFFECT:
+ {
+ vector org2;
+ org2 = w_org + w_backoff * 12;
+ pointparticles(particleeffectnum("rocket_explode"), org2, '0 0 0', 1);
+ if(!w_issilent)
+ sound(self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_sound("weapons/rocket_impact.wav");
+ return TRUE;
+ }
+ }
+ return TRUE;
+}
+#endif
+#endif
--- /dev/null
- if(IsDifferentTeam(self.realowner, other))
+#ifdef REGISTER_WEAPON
+REGISTER_WEAPON(
+/* WEP_##id */ ELECTRO,
+/* function */ w_electro,
+/* ammotype */ IT_CELLS,
+/* impulse */ 5,
+/* flags */ WEP_FLAG_NORMAL | WEP_FLAG_RELOADABLE | WEP_TYPE_SPLASH,
+/* rating */ BOT_PICKUP_RATING_MID,
+/* model */ "electro",
+/* netname */ "electro",
+/* fullname */ _("Electro")
+);
+
+#ifdef SVQC
+void ElectroInit();
+vector electro_shotorigin[4];
+#endif
+#else
+#ifdef SVQC
+void spawnfunc_weapon_electro() { weapon_defaultspawnfunc(WEP_ELECTRO); }
+
+.float electro_count;
+.float electro_secondarytime;
+
+void W_Plasma_Explode_Combo (void);
+
+void W_Plasma_TriggerCombo(vector org, float rad, entity own)
+{
+ entity e;
+ e = WarpZone_FindRadius(org, rad, TRUE);
+ while (e)
+ {
+ if (e.classname == "plasma")
+ {
+ // change owner to whoever caused the combo explosion
+ e.realowner = own;
+ e.takedamage = DAMAGE_NO;
+ e.classname = "plasma_chain";
+ e.think = W_Plasma_Explode_Combo;
+ e.nextthink = time + vlen(e.WarpZone_findradius_dist) / autocvar_g_balance_electro_combo_speed; // delay combo chains, looks cooler
+ }
+ e = e.chain;
+ }
+}
+
+void W_Plasma_Explode (void)
+{
+ if(other.takedamage == DAMAGE_AIM)
+ if(IS_PLAYER(other))
++ if(DIFF_TEAM(self.realowner, other))
+ if(other.deadflag == DEAD_NO)
+ if(IsFlying(other))
+ Send_Notification(NOTIF_ONE, self.realowner, MSG_ANNCE, ANNCE_ACHIEVEMENT_ELECTROBITCH);
+
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+ if (self.movetype == MOVETYPE_BOUNCE)
+ {
+ RadiusDamage (self, self.realowner, autocvar_g_balance_electro_secondary_damage, autocvar_g_balance_electro_secondary_edgedamage, autocvar_g_balance_electro_secondary_radius, world, world, autocvar_g_balance_electro_secondary_force, self.projectiledeathtype, other);
+ }
+ else
+ {
+ W_Plasma_TriggerCombo(self.origin, autocvar_g_balance_electro_primary_comboradius, self.realowner);
+ RadiusDamage (self, self.realowner, autocvar_g_balance_electro_primary_damage, autocvar_g_balance_electro_primary_edgedamage, autocvar_g_balance_electro_primary_radius, world, world, autocvar_g_balance_electro_primary_force, self.projectiledeathtype, other);
+ }
+
+ remove (self);
+}
+
+void W_Plasma_Explode_Combo (void)
+{
+ W_Plasma_TriggerCombo(self.origin, autocvar_g_balance_electro_combo_comboradius, self.realowner);
+
+ self.event_damage = func_null;
+ RadiusDamage (self, self.realowner, autocvar_g_balance_electro_combo_damage, autocvar_g_balance_electro_combo_edgedamage, autocvar_g_balance_electro_combo_radius, world, world, autocvar_g_balance_electro_combo_force, WEP_ELECTRO | HITTYPE_BOUNCE, world); // use THIS type for a combo because primary can't bounce
+
+ remove (self);
+}
+
+void W_Plasma_Touch (void)
+{
+ //self.velocity = self.velocity * 0.1;
+
+ PROJECTILE_TOUCH;
+ if (other.takedamage == DAMAGE_AIM) {
+ W_Plasma_Explode ();
+ } else {
+ //UpdateCSQCProjectile(self);
+ spamsound (self, CH_SHOTS, "weapons/electro_bounce.wav", VOL_BASE, ATTEN_NORM);
+ self.projectiledeathtype |= HITTYPE_BOUNCE;
+ }
+}
+
+void W_Plasma_TouchExplode (void)
+{
+ PROJECTILE_TOUCH;
+ W_Plasma_Explode ();
+}
+
+void W_Plasma_Damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+{
+ if(self.health <= 0)
+ return;
+
+ // note: combos are usually triggered by W_Plasma_TriggerCombo, not damage
+ float is_combo = (inflictor.classname == "plasma_chain" || inflictor.classname == "plasma_prim");
+
+ if (!W_CheckProjectileDamage(inflictor.realowner, self.realowner, deathtype, (is_combo ? 1 : -1)))
+ return; // g_projectiles_damage says to halt
+
+ self.health = self.health - damage;
+ if (self.health <= 0)
+ {
+ self.takedamage = DAMAGE_NO;
+ self.nextthink = time;
+ if (is_combo)
+ {
+ // change owner to whoever caused the combo explosion
+ self.realowner = inflictor.realowner;
+ self.classname = "plasma_chain";
+ self.think = W_Plasma_Explode_Combo;
+ self.nextthink = time + min(autocvar_g_balance_electro_combo_radius, vlen(self.origin - inflictor.origin)) / autocvar_g_balance_electro_combo_speed; // delay combo chains, looks cooler
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ bounding the length, because inflictor may be in a galaxy far far away (warpzones)
+ }
+ else
+ {
+ self.use = W_Plasma_Explode;
+ self.think = adaptor_think2use; // not _hittype_splash, as this runs "immediately"
+ }
+ }
+}
+
+void W_Electro_Attack()
+{
+ entity proj;
+
+ W_DecreaseAmmo(ammo_cells, autocvar_g_balance_electro_primary_ammo, autocvar_g_balance_electro_reload_ammo);
+
+ W_SetupShot_ProjectileSize (self, '0 0 -3', '0 0 -3', FALSE, 2, "weapons/electro_fire.wav", CH_WEAPON_A, autocvar_g_balance_electro_primary_damage);
+
+ pointparticles(particleeffectnum("electro_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+ proj = spawn ();
+ proj.classname = "plasma_prim";
+ proj.owner = proj.realowner = self;
+ proj.bot_dodge = TRUE;
+ proj.bot_dodgerating = autocvar_g_balance_electro_primary_damage;
+ proj.use = W_Plasma_Explode;
+ proj.think = adaptor_think2use_hittype_splash;
+ proj.nextthink = time + autocvar_g_balance_electro_primary_lifetime;
+ PROJECTILE_MAKETRIGGER(proj);
+ proj.projectiledeathtype = WEP_ELECTRO;
+ setorigin(proj, w_shotorg);
+
+ proj.movetype = MOVETYPE_FLY;
+ W_SETUPPROJECTILEVELOCITY(proj, g_balance_electro_primary);
+ proj.angles = vectoangles(proj.velocity);
+ proj.touch = W_Plasma_TouchExplode;
+ setsize(proj, '0 0 -3', '0 0 -3');
+ proj.flags = FL_PROJECTILE;
+ proj.missile_flags = MIF_SPLASH;
+
+ CSQCProjectile(proj, TRUE, PROJECTILE_ELECTRO_BEAM, TRUE);
+
+ other = proj; MUTATOR_CALLHOOK(EditProjectile);
+}
+
+void W_Electro_Attack2()
+{
+ entity proj;
+
+ W_DecreaseAmmo(ammo_cells, autocvar_g_balance_electro_secondary_ammo, autocvar_g_balance_electro_reload_ammo);
+
+ W_SetupShot_ProjectileSize (self, '0 0 -4', '0 0 -4', FALSE, 2, "weapons/electro_fire2.wav", CH_WEAPON_A, autocvar_g_balance_electro_secondary_damage);
+
+ w_shotdir = v_forward; // no TrueAim for grenades please
+
+ pointparticles(particleeffectnum("electro_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+ proj = spawn ();
+ proj.classname = "plasma";
+ proj.owner = proj.realowner = self;
+ proj.use = W_Plasma_Explode;
+ proj.think = adaptor_think2use_hittype_splash;
+ proj.bot_dodge = TRUE;
+ proj.bot_dodgerating = autocvar_g_balance_electro_secondary_damage;
+ proj.nextthink = time + autocvar_g_balance_electro_secondary_lifetime;
+ PROJECTILE_MAKETRIGGER(proj);
+ proj.projectiledeathtype = WEP_ELECTRO | HITTYPE_SECONDARY;
+ setorigin(proj, w_shotorg);
+
+ //proj.glow_size = 50;
+ //proj.glow_color = 45;
+ proj.movetype = MOVETYPE_BOUNCE;
+ W_SETUPPROJECTILEVELOCITY_UP(proj, g_balance_electro_secondary);
+ proj.touch = W_Plasma_Touch;
+ setsize(proj, '0 0 -4', '0 0 -4');
+ proj.takedamage = DAMAGE_YES;
+ proj.damageforcescale = autocvar_g_balance_electro_secondary_damageforcescale;
+ proj.health = autocvar_g_balance_electro_secondary_health;
+ proj.event_damage = W_Plasma_Damage;
+ proj.flags = FL_PROJECTILE;
+ proj.damagedbycontents = (autocvar_g_balance_electro_secondary_damagedbycontents);
+
+ proj.bouncefactor = autocvar_g_balance_electro_secondary_bouncefactor;
+ proj.bouncestop = autocvar_g_balance_electro_secondary_bouncestop;
+ proj.missile_flags = MIF_SPLASH | MIF_ARC;
+
+#if 0
+ entity p2;
+ p2 = spawn();
+ copyentity(proj, p2);
+ setmodel(p2, "models/ebomb.mdl");
+ setsize(p2, proj.mins, proj.maxs);
+#endif
+
+ CSQCProjectile(proj, TRUE, PROJECTILE_ELECTRO, FALSE); // no culling, it has sound
+
+ other = proj; MUTATOR_CALLHOOK(EditProjectile);
+}
+
+.vector hook_start, hook_end;
+float lgbeam_send(entity to, float sf)
+{
+ WriteByte(MSG_ENTITY, ENT_CLIENT_ELECTRO_BEAM);
+ sf = sf & 0x7F;
+ if(sound_allowed(MSG_BROADCAST, self.realowner))
+ sf |= 0x80;
+ WriteByte(MSG_ENTITY, sf);
+ if(sf & 1)
+ {
+ WriteByte(MSG_ENTITY, num_for_edict(self.realowner));
+ WriteCoord(MSG_ENTITY, autocvar_g_balance_electro_primary_range);
+ }
+ if(sf & 2)
+ {
+ WriteCoord(MSG_ENTITY, self.hook_start_x);
+ WriteCoord(MSG_ENTITY, self.hook_start_y);
+ WriteCoord(MSG_ENTITY, self.hook_start_z);
+ }
+ if(sf & 4)
+ {
+ WriteCoord(MSG_ENTITY, self.hook_end_x);
+ WriteCoord(MSG_ENTITY, self.hook_end_y);
+ WriteCoord(MSG_ENTITY, self.hook_end_z);
+ }
+ return TRUE;
+}
+.entity lgbeam;
+.float prevlgfire;
+float lgbeam_checkammo()
+{
+ if(self.realowner.items & IT_UNLIMITED_WEAPON_AMMO)
+ return TRUE;
+ else if(autocvar_g_balance_electro_reload_ammo)
+ return self.realowner.clip_load > 0;
+ else
+ return self.realowner.ammo_cells > 0;
+}
+
+entity lgbeam_owner_ent;
+void lgbeam_think()
+{
+ entity owner_player;
+ owner_player = self.realowner;
+
+ owner_player.prevlgfire = time;
+ if (self != owner_player.lgbeam)
+ {
+ remove(self);
+ return;
+ }
+
+ if (owner_player.weaponentity.state != WS_INUSE || !lgbeam_checkammo() || owner_player.deadflag != DEAD_NO || !owner_player.BUTTON_ATCK || owner_player.freezetag_frozen)
+ {
+ if(self == owner_player.lgbeam)
+ owner_player.lgbeam = world;
+ remove(self);
+ return;
+ }
+
+ self.nextthink = time;
+
+ makevectors(owner_player.v_angle);
+
+ float dt, f;
+ dt = frametime;
+
+ // if this weapon is reloadable, decrease its load. Else decrease the player's ammo
+ if not(owner_player.items & IT_UNLIMITED_WEAPON_AMMO)
+ {
+ if(autocvar_g_balance_electro_primary_ammo)
+ {
+ if(autocvar_g_balance_electro_reload_ammo)
+ {
+ dt = min(dt, owner_player.clip_load / autocvar_g_balance_electro_primary_ammo);
+ owner_player.clip_load = max(0, owner_player.clip_load - autocvar_g_balance_electro_primary_ammo * frametime);
+ owner_player.(weapon_load[WEP_ELECTRO]) = owner_player.clip_load;
+ }
+ else
+ {
+ dt = min(dt, owner_player.ammo_cells / autocvar_g_balance_electro_primary_ammo);
+ owner_player.ammo_cells = max(0, owner_player.ammo_cells - autocvar_g_balance_electro_primary_ammo * frametime);
+ }
+ }
+ }
+
+ W_SetupShot_Range(owner_player, TRUE, 0, "", 0, autocvar_g_balance_electro_primary_damage * dt, autocvar_g_balance_electro_primary_range);
+ if(!lgbeam_owner_ent)
+ {
+ lgbeam_owner_ent = spawn();
+ lgbeam_owner_ent.classname = "lgbeam_owner_ent";
+ }
+ WarpZone_traceline_antilag(lgbeam_owner_ent, w_shotorg, w_shotend, MOVE_NORMAL, lgbeam_owner_ent, ANTILAG_LATENCY(owner_player));
+
+ // apply the damage
+ if(trace_ent)
+ {
+ vector force;
+ force = w_shotdir * autocvar_g_balance_electro_primary_force + '0 0 1' * autocvar_g_balance_electro_primary_force_up;
+
+ f = ExponentialFalloff(autocvar_g_balance_electro_primary_falloff_mindist, autocvar_g_balance_electro_primary_falloff_maxdist, autocvar_g_balance_electro_primary_falloff_halflifedist, vlen(WarpZone_UnTransformOrigin(WarpZone_trace_transform, trace_endpos) - w_shotorg));
+
+ if(accuracy_isgooddamage(owner_player, trace_ent))
+ accuracy_add(owner_player, WEP_ELECTRO, 0, autocvar_g_balance_electro_primary_damage * dt * f);
+ Damage (trace_ent, owner_player, owner_player, autocvar_g_balance_electro_primary_damage * dt * f, WEP_ELECTRO, trace_endpos, force * dt);
+ }
+ W_Plasma_TriggerCombo(trace_endpos, autocvar_g_balance_electro_primary_comboradius, owner_player);
+
+ // draw effect
+ if(w_shotorg != self.hook_start)
+ {
+ self.SendFlags |= 2;
+ self.hook_start = w_shotorg;
+ }
+ if(w_shotend != self.hook_end)
+ {
+ self.SendFlags |= 4;
+ self.hook_end = w_shotend;
+ }
+}
+
+// experimental lightning gun
+void W_Electro_Attack3 (void)
+{
+ // only play fire sound if 0.5 sec has passed since player let go the fire button
+ if(time - self.prevlgfire > 0.5)
+ sound (self, CH_WEAPON_A, "weapons/lgbeam_fire.wav", VOL_BASE, ATTEN_NORM);
+
+ entity beam, oldself;
+
+ self.lgbeam = beam = spawn();
+ beam.classname = "lgbeam";
+ beam.solid = SOLID_NOT;
+ beam.think = lgbeam_think;
+ beam.owner = beam.realowner = self;
+ beam.movetype = MOVETYPE_NONE;
+ beam.shot_spread = 0;
+ beam.bot_dodge = TRUE;
+ beam.bot_dodgerating = autocvar_g_balance_electro_primary_damage;
+ Net_LinkEntity(beam, FALSE, 0, lgbeam_send);
+
+ oldself = self;
+ self = beam;
+ self.think();
+ self = oldself;
+}
+
+void ElectroInit()
+{
+ WEP_ACTION(WEP_ELECTRO, WR_INIT);
+ electro_shotorigin[0] = shotorg_adjust_values(CL_Weapon_GetShotOrg(WEP_ELECTRO), FALSE, FALSE, 1);
+ electro_shotorigin[1] = shotorg_adjust_values(CL_Weapon_GetShotOrg(WEP_ELECTRO), FALSE, FALSE, 2);
+ electro_shotorigin[2] = shotorg_adjust_values(CL_Weapon_GetShotOrg(WEP_ELECTRO), FALSE, FALSE, 3);
+ electro_shotorigin[3] = shotorg_adjust_values(CL_Weapon_GetShotOrg(WEP_ELECTRO), FALSE, FALSE, 4);
+}
+
+void w_electro_checkattack()
+{
+ if(self.electro_count > 1)
+ if(self.BUTTON_ATCK2)
+ if(weapon_prepareattack(1, -1))
+ {
+ W_Electro_Attack2();
+ self.electro_count -= 1;
+ weapon_thinkf(WFRAME_FIRE2, autocvar_g_balance_electro_secondary_animtime, w_electro_checkattack);
+ return;
+ }
+
+ w_ready();
+}
+
+.float bot_secondary_electromooth;
+.float BUTTON_ATCK_prev;
+float w_electro(float req)
+{
+ float ammo_amount;
+ switch(req)
+ {
+ case WR_AIM:
+ {
+ self.BUTTON_ATCK=FALSE;
+ self.BUTTON_ATCK2=FALSE;
+ if(vlen(self.origin-self.enemy.origin) > 1000)
+ self.bot_secondary_electromooth = 0;
+ if(self.bot_secondary_electromooth == 0)
+ {
+ float shoot;
+
+ if(autocvar_g_balance_electro_primary_speed)
+ shoot = bot_aim(autocvar_g_balance_electro_primary_speed, 0, autocvar_g_balance_electro_primary_lifetime, FALSE);
+ else
+ shoot = bot_aim(1000000, 0, 0.001, FALSE);
+
+ if(shoot)
+ {
+ self.BUTTON_ATCK = TRUE;
+ if(random() < 0.01) self.bot_secondary_electromooth = 1;
+ }
+ }
+ else
+ {
+ if(bot_aim(autocvar_g_balance_electro_secondary_speed, autocvar_g_balance_mortar_secondary_speed_up, autocvar_g_balance_electro_secondary_lifetime, TRUE)) // WHAT THE ACTUAL FUUUUUUUUUCK?!?!? WEAPONTODO
+ {
+ self.BUTTON_ATCK2 = TRUE;
+ if(random() < 0.03) self.bot_secondary_electromooth = 0;
+ }
+ }
+
+ return TRUE;
+ }
+ case WR_THINK:
+ {
+ if(autocvar_g_balance_electro_reload_ammo) // forced reload
+ {
+ ammo_amount = 0;
+ if(autocvar_g_balance_electro_lightning)
+ {
+ if(self.clip_load > 0)
+ ammo_amount = 1;
+ }
+ else if(self.clip_load >= autocvar_g_balance_electro_primary_ammo)
+ ammo_amount = 1;
+ if(self.clip_load >= autocvar_g_balance_electro_secondary_ammo)
+ ammo_amount += 1;
+
+ if(!ammo_amount)
+ {
+ WEP_ACTION(self.weapon, WR_RELOAD);
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+ if (self.BUTTON_ATCK)
+ {
+ if(autocvar_g_balance_electro_lightning)
+ if(self.BUTTON_ATCK_prev)
+ weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_electro_primary_animtime, w_ready);
+
+ if (weapon_prepareattack(0, (autocvar_g_balance_electro_lightning ? 0 : autocvar_g_balance_electro_primary_refire)))
+ {
+ if(autocvar_g_balance_electro_lightning)
+ {
+ if ((!self.lgbeam) || wasfreed(self.lgbeam))
+ {
+ W_Electro_Attack3();
+ }
+ if(!self.BUTTON_ATCK_prev)
+ {
+ weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_electro_primary_animtime, w_ready);
+ self.BUTTON_ATCK_prev = 1;
+ }
+ }
+ else
+ {
+ W_Electro_Attack();
+ weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_electro_primary_animtime, w_ready);
+ }
+ }
+ } else {
+ if(autocvar_g_balance_electro_lightning)
+ {
+ if (self.BUTTON_ATCK_prev != 0)
+ {
+ weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_electro_primary_animtime, w_ready);
+ ATTACK_FINISHED(self) = time + autocvar_g_balance_electro_primary_refire * W_WeaponRateFactor();
+ }
+ self.BUTTON_ATCK_prev = 0;
+ }
+
+ if (self.BUTTON_ATCK2)
+ {
+ if (time >= self.electro_secondarytime)
+ if (weapon_prepareattack(1, autocvar_g_balance_electro_secondary_refire))
+ {
+ W_Electro_Attack2();
+ self.electro_count = autocvar_g_balance_electro_secondary_count;
+ weapon_thinkf(WFRAME_FIRE2, autocvar_g_balance_electro_secondary_animtime, w_electro_checkattack);
+ self.electro_secondarytime = time + autocvar_g_balance_electro_secondary_refire2 * W_WeaponRateFactor();
+ }
+ }
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_model ("models/weapons/g_electro.md3");
+ precache_model ("models/weapons/v_electro.md3");
+ precache_model ("models/weapons/h_electro.iqm");
+ precache_sound ("weapons/electro_bounce.wav");
+ precache_sound ("weapons/electro_fire.wav");
+ precache_sound ("weapons/electro_fire2.wav");
+ precache_sound ("weapons/electro_impact.wav");
+ precache_sound ("weapons/electro_impact_combo.wav");
+
+ if(autocvar_g_balance_electro_lightning)
+ precache_sound ("weapons/lgbeam_fire.wav");
+
+ return TRUE;
+ }
+ case WR_SETUP:
+ {
+ self.current_ammo = ammo_cells;
+ return TRUE;
+ }
+ case WR_CHECKAMMO1:
+ {
+ if(autocvar_g_balance_electro_lightning)
+ {
+ if(!autocvar_g_balance_electro_primary_ammo)
+ ammo_amount = 1;
+ else
+ ammo_amount = self.ammo_cells > 0;
+ ammo_amount += self.(weapon_load[WEP_ELECTRO]) > 0;
+ }
+ else
+ {
+ ammo_amount = self.ammo_cells >= autocvar_g_balance_electro_primary_ammo;
+ ammo_amount += self.(weapon_load[WEP_ELECTRO]) >= autocvar_g_balance_electro_primary_ammo;
+ }
+ return ammo_amount;
+ }
+ case WR_CHECKAMMO2:
+ {
+ if(autocvar_g_balance_electro_combo_safeammocheck) // true if you can fire at least one secondary blob AND one primary shot after it, otherwise false.
+ {
+ ammo_amount = self.ammo_cells >= autocvar_g_balance_electro_secondary_ammo + autocvar_g_balance_electro_primary_ammo;
+ ammo_amount += self.(weapon_load[WEP_ELECTRO]) >= autocvar_g_balance_electro_secondary_ammo + autocvar_g_balance_electro_primary_ammo;
+ }
+ else
+ {
+ ammo_amount = self.ammo_cells >= autocvar_g_balance_electro_secondary_ammo;
+ ammo_amount += self.(weapon_load[WEP_ELECTRO]) >= autocvar_g_balance_electro_secondary_ammo;
+ }
+ return ammo_amount;
+ }
+ case WR_RESETPLAYER:
+ {
+ self.electro_secondarytime = time;
+ return TRUE;
+ }
+ case WR_RELOAD:
+ {
+ W_Reload(min(autocvar_g_balance_electro_primary_ammo, autocvar_g_balance_electro_secondary_ammo), "weapons/reload.wav");
+ return TRUE;
+ }
+ case WR_SUICIDEMESSAGE:
+ {
+ if(w_deathtype & HITTYPE_SECONDARY)
+ return WEAPON_ELECTRO_SUICIDE_ORBS;
+ else
+ return WEAPON_ELECTRO_SUICIDE_BOLT;
+ }
+ case WR_KILLMESSAGE:
+ {
+ if(w_deathtype & HITTYPE_SECONDARY)
+ {
+ return WEAPON_ELECTRO_MURDER_ORBS;
+ }
+ else
+ {
+ if(w_deathtype & HITTYPE_BOUNCE)
+ return WEAPON_ELECTRO_MURDER_COMBO;
+ else
+ return WEAPON_ELECTRO_MURDER_BOLT;
+ }
+ }
+ }
+ return TRUE;
+}
+#endif
+#ifdef CSQC
+float w_electro(float req)
+{
+ switch(req)
+ {
+ case WR_IMPACTEFFECT:
+ {
+ vector org2;
+ org2 = w_org + w_backoff * 6;
+ if(w_deathtype & HITTYPE_SECONDARY)
+ {
+ pointparticles(particleeffectnum("electro_ballexplode"), org2, '0 0 0', 1);
+ if(!w_issilent)
+ sound(self, CH_SHOTS, "weapons/electro_impact.wav", VOL_BASE, ATTEN_NORM);
+ }
+ else
+ {
+ if(w_deathtype & HITTYPE_BOUNCE)
+ {
+ // this is sent as "primary (w_deathtype & HITTYPE_BOUNCE)" to distinguish it from (w_deathtype & HITTYPE_SECONDARY) bounced balls
+ pointparticles(particleeffectnum("electro_combo"), org2, '0 0 0', 1);
+ if(!w_issilent)
+ sound(self, CH_SHOTS, "weapons/electro_impact_combo.wav", VOL_BASE, ATTEN_NORM);
+ }
+ else
+ {
+ pointparticles(particleeffectnum("electro_impact"), org2, '0 0 0', 1);
+ if(!w_issilent)
+ sound(self, CH_SHOTS, "weapons/electro_impact.wav", VOL_BASE, ATTEN_NORM);
+ }
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_sound("weapons/electro_impact.wav");
+ precache_sound("weapons/electro_impact_combo.wav");
+ return TRUE;
+ }
+ }
+ return TRUE;
+}
+#endif
+#endif
--- /dev/null
- if(e != self.realowner) if(e.takedamage == DAMAGE_AIM) if(!IS_PLAYER(e) || !self.realowner || IsDifferentTeam(e, self))
+#ifdef REGISTER_WEAPON
+REGISTER_WEAPON(
+/* WEP_##id */ FIREBALL,
+/* function */ w_fireball,
+/* ammotype */ 0,
+/* impulse */ 9,
+/* flags */ WEP_FLAG_SUPERWEAPON | WEP_TYPE_SPLASH,
+/* rating */ BOT_PICKUP_RATING_MID,
+/* model */ "fireball",
+/* netname */ "fireball",
+/* fullname */ _("Fireball")
+);
+#define FIREBALL_SETTINGS(weapon) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, animtime) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, refire) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, damage) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, damageforcescale) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, speed) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, lifetime) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, laserburntime) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, laserdamage) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, laseredgedamage) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, laserradius) \
+ WEP_ADD_CVAR(weapon, MO_PRI, edgedamage) \
+ WEP_ADD_CVAR(weapon, MO_PRI, force) \
+ WEP_ADD_CVAR(weapon, MO_PRI, radius) \
+ WEP_ADD_CVAR(weapon, MO_PRI, health) \
+ WEP_ADD_CVAR(weapon, MO_PRI, refire2) \
+ WEP_ADD_CVAR(weapon, MO_PRI, bfgdamage) \
+ WEP_ADD_CVAR(weapon, MO_PRI, bfgforce) \
+ WEP_ADD_CVAR(weapon, MO_PRI, bfgradius) \
+ WEP_ADD_CVAR(weapon, MO_SEC, damagetime) \
+ WEP_ADD_CVAR(weapon, MO_SEC, speed_up) \
+ WEP_ADD_PROP(weapon, reloading_ammo, reload_ammo) \
+ WEP_ADD_PROP(weapon, reloading_time, reload_time) \
+ WEP_ADD_PROP(weapon, switchdelay_raise, switchdelay_raise) \
+ WEP_ADD_PROP(weapon, switchdelay_drop, switchdelay_drop)
+
+#ifdef SVQC
+FIREBALL_SETTINGS(fireball)
+.float bot_primary_fireballmooth; // whatever a mooth is
+.vector fireball_impactvec;
+.float fireball_primarytime;
+#endif
+#else
+#ifdef SVQC
+void spawnfunc_weapon_fireball() { weapon_defaultspawnfunc(WEP_FIREBALL); }
+
+void W_Fireball_Explode (void)
+{
+ entity e;
+ float dist;
+ float points;
+ vector dir;
+ float d;
+
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+
+ // 1. dist damage
+ d = (self.realowner.health + self.realowner.armorvalue);
+ RadiusDamage (self, self.realowner, WEP_CVAR_PRI(fireball, damage), WEP_CVAR_PRI(fireball, edgedamage), WEP_CVAR_PRI(fireball, radius), world, world, WEP_CVAR_PRI(fireball, force), self.projectiledeathtype, other);
+ if(self.realowner.health + self.realowner.armorvalue >= d)
+ if(!self.cnt)
+ {
+ modeleffect_spawn("models/sphere/sphere.md3", 0, 0, self.origin, '0 0 0', '0 0 0', '0 0 0', 0, WEP_CVAR_PRI(fireball, bfgradius), 0.2, 0.05, 0.25);
+
+ // 2. bfg effect
+ // NOTE: this cannot be made warpzone aware by design. So, better intentionally ignore warpzones here.
+ for(e = findradius(self.origin, WEP_CVAR_PRI(fireball, bfgradius)); e; e = e.chain)
- if(e != self.realowner) if(e.takedamage == DAMAGE_AIM) if(!IS_PLAYER(e) || !self.realowner || IsDifferentTeam(e, self))
++ if(e != self.realowner) if(e.takedamage == DAMAGE_AIM) if(!IS_PLAYER(e) || !self.realowner || DIFF_TEAM(e, self))
+ {
+ // can we see fireball?
+ traceline(e.origin + e.view_ofs, self.origin, MOVE_NORMAL, e);
+ if(/* trace_startsolid || */ trace_fraction != 1) // startsolid should be never happening anyway
+ continue;
+ // can we see player who shot fireball?
+ traceline(e.origin + e.view_ofs, self.realowner.origin + self.realowner.view_ofs, MOVE_NORMAL, e);
+ if(trace_ent != self.realowner)
+ if(/* trace_startsolid || */ trace_fraction != 1)
+ continue;
+ dist = vlen(self.origin - e.origin - e.view_ofs);
+ points = (1 - sqrt(dist / WEP_CVAR_PRI(fireball, bfgradius)));
+ if(points <= 0)
+ continue;
+ dir = normalize(e.origin + e.view_ofs - self.origin);
+
+ if(accuracy_isgooddamage(self.realowner, e))
+ accuracy_add(self.realowner, WEP_FIREBALL, 0, WEP_CVAR_PRI(fireball, bfgdamage) * points);
+
+ Damage(e, self, self.realowner, WEP_CVAR_PRI(fireball, bfgdamage) * points, self.projectiledeathtype | HITTYPE_BOUNCE | HITTYPE_SPLASH, e.origin + e.view_ofs, WEP_CVAR_PRI(fireball, bfgforce) * dir);
+ pointparticles(particleeffectnum("fireball_bfgdamage"), e.origin, -1 * dir, 1);
+ }
+ }
+
+ remove (self);
+}
+
+void W_Fireball_TouchExplode (void)
+{
+ PROJECTILE_TOUCH;
+ W_Fireball_Explode ();
+}
+
+void W_Fireball_LaserPlay(float dt, float dist, float damage, float edgedamage, float burntime)
+{
+ entity e;
+ float d;
+ vector p;
+
+ if(damage <= 0)
+ return;
+
+ RandomSelection_Init();
+ for(e = WarpZone_FindRadius(self.origin, dist, TRUE); e; e = e.chain)
++ if(e != self.realowner) if(e.takedamage == DAMAGE_AIM) if(!IS_PLAYER(e) || !self.realowner || DIFF_TEAM(e, self))
+ {
+ p = e.origin;
+ p_x += e.mins_x + random() * (e.maxs_x - e.mins_x);
+ p_y += e.mins_y + random() * (e.maxs_y - e.mins_y);
+ p_z += e.mins_z + random() * (e.maxs_z - e.mins_z);
+ d = vlen(WarpZone_UnTransformOrigin(e, self.origin) - p);
+ if(d < dist)
+ {
+ e.fireball_impactvec = p;
+ RandomSelection_Add(e, 0, string_null, 1 / (1 + d), !Fire_IsBurning(e));
+ }
+ }
+ if(RandomSelection_chosen_ent)
+ {
+ d = vlen(WarpZone_UnTransformOrigin(RandomSelection_chosen_ent, self.origin) - RandomSelection_chosen_ent.fireball_impactvec);
+ d = damage + (edgedamage - damage) * (d / dist);
+ Fire_AddDamage(RandomSelection_chosen_ent, self.realowner, d * burntime, burntime, self.projectiledeathtype | HITTYPE_BOUNCE);
+ //trailparticles(self, particleeffectnum("fireball_laser"), self.origin, RandomSelection_chosen_ent.fireball_impactvec);
+ pointparticles(particleeffectnum("fireball_laser"), self.origin, RandomSelection_chosen_ent.fireball_impactvec - self.origin, 1);
+ }
+}
+
+void W_Fireball_Think()
+{
+ if(time > self.pushltime)
+ {
+ self.cnt = 1;
+ self.projectiledeathtype |= HITTYPE_SPLASH;
+ W_Fireball_Explode();
+ return;
+ }
+
+ W_Fireball_LaserPlay(0.1, WEP_CVAR_PRI(fireball, laserradius), WEP_CVAR_PRI(fireball, laserdamage), WEP_CVAR_PRI(fireball, laseredgedamage), WEP_CVAR_PRI(fireball, laserburntime));
+
+ self.nextthink = time + 0.1;
+}
+
+void W_Fireball_Damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+{
+ if(self.health <= 0)
+ return;
+
+ if (!W_CheckProjectileDamage(inflictor.realowner, self.realowner, deathtype, -1)) // no exceptions
+ return; // g_projectiles_damage says to halt
+
+ self.health = self.health - damage;
+ if (self.health <= 0)
+ {
+ self.cnt = 1;
+ W_PrepareExplosionByDamage(attacker, W_Fireball_Explode);
+ }
+}
+
+void W_Fireball_Attack1()
+{
+ entity proj;
+
+ W_SetupShot_ProjectileSize (self, '-16 -16 -16', '16 16 16', FALSE, 2, "weapons/fireball_fire2.wav", CH_WEAPON_A, WEP_CVAR_PRI(fireball, damage) + WEP_CVAR_PRI(fireball, bfgdamage));
+
+ pointparticles(particleeffectnum("fireball_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+ proj = spawn ();
+ proj.classname = "plasma_prim";
+ proj.owner = proj.realowner = self;
+ proj.bot_dodge = TRUE;
+ proj.bot_dodgerating = WEP_CVAR_PRI(fireball, damage);
+ proj.pushltime = time + WEP_CVAR_PRI(fireball, lifetime);
+ proj.use = W_Fireball_Explode;
+ proj.think = W_Fireball_Think;
+ proj.nextthink = time;
+ proj.health = WEP_CVAR_PRI(fireball, health);
+ proj.team = self.team;
+ proj.event_damage = W_Fireball_Damage;
+ proj.takedamage = DAMAGE_YES;
+ proj.damageforcescale = WEP_CVAR_PRI(fireball, damageforcescale);
+ PROJECTILE_MAKETRIGGER(proj);
+ proj.projectiledeathtype = WEP_FIREBALL;
+ setorigin(proj, w_shotorg);
+
+ proj.movetype = MOVETYPE_FLY;
+ W_SETUPPROJECTILEVELOCITY(proj, g_balance_fireball_primary);
+ proj.angles = vectoangles(proj.velocity);
+ proj.touch = W_Fireball_TouchExplode;
+ setsize(proj, '-16 -16 -16', '16 16 16');
+ proj.flags = FL_PROJECTILE;
+ proj.missile_flags = MIF_SPLASH | MIF_PROXY;
+
+ CSQCProjectile(proj, TRUE, PROJECTILE_FIREBALL, TRUE);
+
+ other = proj; MUTATOR_CALLHOOK(EditProjectile);
+}
+
+void W_Fireball_AttackEffect(float i, vector f_diff)
+{
+ W_SetupShot_ProjectileSize (self, '-16 -16 -16', '16 16 16', FALSE, 0, "", 0, 0);
+ w_shotorg += f_diff_x * v_up + f_diff_y * v_right;
+ pointparticles(particleeffectnum("fireball_preattack_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+}
+
+void W_Fireball_Attack1_Frame4()
+{
+ W_Fireball_Attack1();
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR_PRI(fireball, animtime), w_ready);
+}
+
+void W_Fireball_Attack1_Frame3()
+{
+ W_Fireball_AttackEffect(0, '+1.25 +3.75 0');
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR_PRI(fireball, animtime), W_Fireball_Attack1_Frame4);
+}
+
+void W_Fireball_Attack1_Frame2()
+{
+ W_Fireball_AttackEffect(0, '-1.25 +3.75 0');
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR_PRI(fireball, animtime), W_Fireball_Attack1_Frame3);
+}
+
+void W_Fireball_Attack1_Frame1()
+{
+ W_Fireball_AttackEffect(1, '+1.25 -3.75 0');
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR_PRI(fireball, animtime), W_Fireball_Attack1_Frame2);
+}
+
+void W_Fireball_Attack1_Frame0()
+{
+ W_Fireball_AttackEffect(0, '-1.25 -3.75 0');
+ sound (self, CH_WEAPON_SINGLE, "weapons/fireball_prefire2.wav", VOL_BASE, ATTEN_NORM);
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR_PRI(fireball, animtime), W_Fireball_Attack1_Frame1);
+}
+
+void W_Firemine_Think()
+{
+ if(time > self.pushltime)
+ {
+ remove(self);
+ return;
+ }
+
+ // make it "hot" once it leaves its owner
+ if(self.owner)
+ {
+ if(vlen(self.origin - self.owner.origin - self.owner.view_ofs) > WEP_CVAR_SEC(fireball, laserradius))
+ {
+ self.cnt += 1;
+ if(self.cnt == 3)
+ self.owner = world;
+ }
+ else
+ self.cnt = 0;
+ }
+
+ W_Fireball_LaserPlay(0.1, WEP_CVAR_SEC(fireball, laserradius), WEP_CVAR_SEC(fireball, laserdamage), WEP_CVAR_SEC(fireball, laseredgedamage), WEP_CVAR_SEC(fireball, laserburntime));
+
+ self.nextthink = time + 0.1;
+}
+
+void W_Firemine_Touch (void)
+{
+ PROJECTILE_TOUCH;
+ if (other.takedamage == DAMAGE_AIM)
+ if(Fire_AddDamage(other, self.realowner, WEP_CVAR_SEC(fireball, damage), WEP_CVAR_SEC(fireball, damagetime), self.projectiledeathtype) >= 0)
+ {
+ remove(self);
+ return;
+ }
+ self.projectiledeathtype |= HITTYPE_BOUNCE;
+}
+
+void W_Fireball_Attack2()
+{
+ entity proj;
+ vector f_diff;
+ float c;
+
+ c = mod(self.bulletcounter, 4);
+ switch(c)
+ {
+ case 0:
+ f_diff = '-1.25 -3.75 0';
+ break;
+ case 1:
+ f_diff = '+1.25 -3.75 0';
+ break;
+ case 2:
+ f_diff = '-1.25 +3.75 0';
+ break;
+ case 3:
+ default:
+ f_diff = '+1.25 +3.75 0';
+ break;
+ }
+ W_SetupShot_ProjectileSize(self, '-4 -4 -4', '4 4 4', FALSE, 2, "weapons/fireball_fire.wav", CH_WEAPON_A, WEP_CVAR_SEC(fireball, damage));
+ traceline(w_shotorg, w_shotorg + f_diff_x * v_up + f_diff_y * v_right, MOVE_NORMAL, self);
+ w_shotorg = trace_endpos;
+
+ pointparticles(particleeffectnum("fireball_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+ proj = spawn ();
+ proj.owner = proj.realowner = self;
+ proj.classname = "grenade";
+ proj.bot_dodge = TRUE;
+ proj.bot_dodgerating = WEP_CVAR_SEC(fireball, damage);
+ proj.movetype = MOVETYPE_BOUNCE;
+ proj.projectiledeathtype = WEP_FIREBALL | HITTYPE_SECONDARY;
+ proj.touch = W_Firemine_Touch;
+ PROJECTILE_MAKETRIGGER(proj);
+ setsize(proj, '-4 -4 -4', '4 4 4');
+ setorigin(proj, w_shotorg);
+ proj.think = W_Firemine_Think;
+ proj.nextthink = time;
+ proj.damageforcescale = WEP_CVAR_SEC(fireball, damageforcescale);
+ proj.pushltime = time + WEP_CVAR_SEC(fireball, lifetime);
+ W_SETUPPROJECTILEVELOCITY_UP(proj, g_balance_fireball_secondary);
+
+ proj.angles = vectoangles(proj.velocity);
+ proj.flags = FL_PROJECTILE;
+ proj.missile_flags = MIF_SPLASH | MIF_PROXY | MIF_ARC;
+
+ CSQCProjectile(proj, TRUE, PROJECTILE_FIREMINE, TRUE);
+
+ other = proj; MUTATOR_CALLHOOK(EditProjectile);
+}
+
+float w_fireball(float req)
+{
+ switch(req)
+ {
+ case WR_AIM:
+ {
+ self.BUTTON_ATCK = FALSE;
+ self.BUTTON_ATCK2 = FALSE;
+ if (self.bot_primary_fireballmooth == 0)
+ {
+ if(bot_aim(WEP_CVAR_PRI(fireball, speed), 0, WEP_CVAR_PRI(fireball, lifetime), FALSE))
+ {
+ self.BUTTON_ATCK = TRUE;
+ if(random() < 0.02) self.bot_primary_fireballmooth = 0;
+ }
+ }
+ else
+ {
+ if(bot_aim(WEP_CVAR_SEC(fireball, speed), WEP_CVAR_SEC(fireball, speed_up), WEP_CVAR_SEC(fireball, lifetime), TRUE))
+ {
+ self.BUTTON_ATCK2 = TRUE;
+ if(random() < 0.01) self.bot_primary_fireballmooth = 1;
+ }
+ }
+
+ return TRUE;
+ }
+ case WR_THINK:
+ {
+ if (self.BUTTON_ATCK)
+ {
+ if (time >= self.fireball_primarytime)
+ if (weapon_prepareattack(0, WEP_CVAR_PRI(fireball, refire)))
+ {
+ W_Fireball_Attack1_Frame0();
+ self.fireball_primarytime = time + WEP_CVAR_PRI(fireball, refire2) * W_WeaponRateFactor();
+ }
+ }
+ else if (self.BUTTON_ATCK2)
+ {
+ if (weapon_prepareattack(1, WEP_CVAR_SEC(fireball, refire)))
+ {
+ W_Fireball_Attack2();
+ weapon_thinkf(WFRAME_FIRE2, WEP_CVAR_SEC(fireball, animtime), w_ready);
+ }
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_model ("models/weapons/g_fireball.md3");
+ precache_model ("models/weapons/v_fireball.md3");
+ precache_model ("models/weapons/h_fireball.iqm");
+ precache_model ("models/sphere/sphere.md3");
+ precache_sound ("weapons/fireball_fire.wav");
+ precache_sound ("weapons/fireball_fire2.wav");
+ precache_sound ("weapons/fireball_prefire2.wav");
+ WEP_SET_PROPS(FIREBALL_SETTINGS(fireball), WEP_FIREBALL)
+ return TRUE;
+ }
+ case WR_SETUP:
+ {
+ self.current_ammo = ammo_none;
+ return TRUE;
+ }
+ case WR_CHECKAMMO1:
+ case WR_CHECKAMMO2:
+ {
+ return TRUE; // fireball has infinite ammo
+ }
+ case WR_CONFIG:
+ {
+ WEP_CONFIG_SETTINGS(FIREBALL_SETTINGS(fireball))
+ return TRUE;
+ }
+ case WR_RESETPLAYER:
+ {
+ self.fireball_primarytime = time;
+ return TRUE;
+ }
+ case WR_SUICIDEMESSAGE:
+ {
+ if(w_deathtype & HITTYPE_SECONDARY)
+ return WEAPON_FIREBALL_SUICIDE_FIREMINE;
+ else
+ return WEAPON_FIREBALL_SUICIDE_BLAST;
+ }
+ case WR_KILLMESSAGE:
+ {
+ if(w_deathtype & HITTYPE_SECONDARY)
+ return WEAPON_FIREBALL_MURDER_FIREMINE;
+ else
+ return WEAPON_FIREBALL_MURDER_BLAST;
+ }
+ }
+ return TRUE;
+}
+#endif
+#ifdef CSQC
+float w_fireball(float req)
+{
+ switch(req)
+ {
+ case WR_IMPACTEFFECT:
+ {
+ vector org2;
+ if(w_deathtype & HITTYPE_SECONDARY)
+ {
+ // firemine goes out silently
+ }
+ else
+ {
+ org2 = w_org + w_backoff * 16;
+ pointparticles(particleeffectnum("fireball_explode"), org2, '0 0 0', 1);
+ if(!w_issilent)
+ sound(self, CH_SHOTS, "weapons/fireball_impact2.wav", VOL_BASE, ATTEN_NORM * 0.25); // long range boom
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_sound("weapons/fireball_impact2.wav");
+ return TRUE;
+ }
+ }
+
+ return TRUE;
+}
+#endif
+#endif
--- /dev/null
- if(IsDifferentTeam(self.realowner, other))
+#ifdef REGISTER_WEAPON
+REGISTER_WEAPON(
+/* WEP_##id */ MINE_LAYER,
+/* function */ w_minelayer,
+/* ammotype */ IT_ROCKETS,
+/* impulse */ 4,
+/* flags */ WEP_FLAG_MUTATORBLOCKED | WEP_FLAG_RELOADABLE | WEP_TYPE_SPLASH,
+/* rating */ BOT_PICKUP_RATING_HIGH,
+/* model */ "minelayer",
+/* netname */ "minelayer",
+/* fullname */ _("Mine Layer")
+);
+
+#define MINELAYER_SETTINGS(weapon) \
+ WEP_ADD_CVAR(weapon, MO_NONE, ammo) \
+ WEP_ADD_CVAR(weapon, MO_NONE, animtime) \
+ WEP_ADD_CVAR(weapon, MO_NONE, damage) \
+ WEP_ADD_CVAR(weapon, MO_NONE, damageforcescale) \
+ WEP_ADD_CVAR(weapon, MO_NONE, detonatedelay) \
+ WEP_ADD_CVAR(weapon, MO_NONE, edgedamage) \
+ WEP_ADD_CVAR(weapon, MO_NONE, force) \
+ WEP_ADD_CVAR(weapon, MO_NONE, health) \
+ WEP_ADD_CVAR(weapon, MO_NONE, lifetime) \
+ WEP_ADD_CVAR(weapon, MO_NONE, lifetime_countdown) \
+ WEP_ADD_CVAR(weapon, MO_NONE, limit) \
+ WEP_ADD_CVAR(weapon, MO_NONE, protection) \
+ WEP_ADD_CVAR(weapon, MO_NONE, proximityradius) \
+ WEP_ADD_CVAR(weapon, MO_NONE, radius) \
+ WEP_ADD_CVAR(weapon, MO_NONE, refire) \
+ WEP_ADD_CVAR(weapon, MO_NONE, remote_damage) \
+ WEP_ADD_CVAR(weapon, MO_NONE, remote_edgedamage) \
+ WEP_ADD_CVAR(weapon, MO_NONE, remote_force) \
+ WEP_ADD_CVAR(weapon, MO_NONE, remote_radius) \
+ WEP_ADD_CVAR(weapon, MO_NONE, speed) \
+ WEP_ADD_CVAR(weapon, MO_NONE, time) \
+ WEP_ADD_PROP(weapon, reloading_ammo, reload_ammo) \
+ WEP_ADD_PROP(weapon, reloading_time, reload_time) \
+ WEP_ADD_PROP(weapon, switchdelay_raise, switchdelay_raise) \
+ WEP_ADD_PROP(weapon, switchdelay_drop, switchdelay_drop)
+
+#ifdef SVQC
+MINELAYER_SETTINGS(minelayer)
+void W_Mine_Think (void);
+.float minelayer_detonate, mine_explodeanyway;
+.float mine_time;
+.vector mine_orientation;
+#endif
+#else
+#ifdef SVQC
+void spawnfunc_weapon_minelayer() { weapon_defaultspawnfunc(WEP_MINE_LAYER); }
+
+void W_Mine_Stick (entity to)
+{
+ spamsound (self, CH_SHOTS, "weapons/mine_stick.wav", VOL_BASE, ATTN_NORM);
+
+ // in order for mines to face properly when sticking to the ground, they must be a server side entity rather than a csqc projectile
+
+ entity newmine;
+ newmine = spawn();
+ newmine.classname = self.classname;
+
+ newmine.bot_dodge = self.bot_dodge;
+ newmine.bot_dodgerating = self.bot_dodgerating;
+
+ newmine.owner = self.owner;
+ newmine.realowner = self.realowner;
+ setsize(newmine, '-4 -4 -4', '4 4 4');
+ setorigin(newmine, self.origin);
+ setmodel(newmine, "models/mine.md3");
+ newmine.angles = vectoangles(-trace_plane_normal); // face against the surface
+
+ newmine.mine_orientation = -trace_plane_normal;
+
+ newmine.takedamage = self.takedamage;
+ newmine.damageforcescale = self.damageforcescale;
+ newmine.health = self.health;
+ newmine.event_damage = self.event_damage;
+ newmine.spawnshieldtime = self.spawnshieldtime;
+ newmine.damagedbycontents = TRUE;
+
+ newmine.movetype = MOVETYPE_NONE; // lock the mine in place
+ newmine.projectiledeathtype = self.projectiledeathtype;
+
+ newmine.mine_time = self.mine_time;
+
+ newmine.touch = func_null;
+ newmine.think = W_Mine_Think;
+ newmine.nextthink = time;
+ newmine.cnt = self.cnt;
+ newmine.flags = self.flags;
+
+ remove(self);
+ self = newmine;
+
+ if(to)
+ SetMovetypeFollow(self, to);
+}
+
+void W_Mine_Explode ()
+{
+ if(other.takedamage == DAMAGE_AIM)
+ if(IS_PLAYER(other))
- if(head == self.realowner || !IsDifferentTeam(head, self.realowner))
++ if(DIFF_TEAM(self.realowner, other))
+ if(other.deadflag == DEAD_NO)
+ if(IsFlying(other))
+ Send_Notification(NOTIF_ONE, self.realowner, MSG_ANNCE, ANNCE_ACHIEVEMENT_AIRSHOT);
+
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+
+ RadiusDamage (self, self.realowner, WEP_CVAR(minelayer, damage), WEP_CVAR(minelayer, edgedamage), WEP_CVAR(minelayer, radius), world, world, WEP_CVAR(minelayer, force), self.projectiledeathtype, other);
+
+ if (self.realowner.weapon == WEP_MINE_LAYER)
+ {
+ entity oldself;
+ oldself = self;
+ self = self.realowner;
+ if (!WEP_ACTION(WEP_MINE_LAYER, WR_CHECKAMMO1))
+ {
+ self.cnt = WEP_MINE_LAYER;
+ ATTACK_FINISHED(self) = time;
+ self.switchweapon = w_getbestweapon(self);
+ }
+ self = oldself;
+ }
+ self.realowner.minelayer_mines -= 1;
+ remove (self);
+}
+
+void W_Mine_DoRemoteExplode ()
+{
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+
+ if(self.movetype == MOVETYPE_NONE || self.movetype == MOVETYPE_FOLLOW)
+ self.velocity = self.mine_orientation; // particle fx and decals need .velocity
+
+ RadiusDamage (self, self.realowner, WEP_CVAR(minelayer, remote_damage), WEP_CVAR(minelayer, remote_edgedamage), WEP_CVAR(minelayer, remote_radius), world, world, WEP_CVAR(minelayer, remote_force), self.projectiledeathtype | HITTYPE_BOUNCE, world);
+
+ if (self.realowner.weapon == WEP_MINE_LAYER)
+ {
+ entity oldself;
+ oldself = self;
+ self = self.realowner;
+ if (!WEP_ACTION(WEP_MINE_LAYER, WR_CHECKAMMO1))
+ {
+ self.cnt = WEP_MINE_LAYER;
+ ATTACK_FINISHED(self) = time;
+ self.switchweapon = w_getbestweapon(self);
+ }
+ self = oldself;
+ }
+ self.realowner.minelayer_mines -= 1;
+ remove (self);
+}
+
+void W_Mine_RemoteExplode ()
+{
+ if(self.realowner.deadflag == DEAD_NO)
+ if((self.spawnshieldtime >= 0)
+ ? (time >= self.spawnshieldtime) // timer
+ : (vlen(NearestPointOnBox(self.realowner, self.origin) - self.origin) > WEP_CVAR(minelayer, remote_radius)) // safety device
+ )
+ {
+ W_Mine_DoRemoteExplode();
+ }
+}
+
+void W_Mine_ProximityExplode ()
+{
+ // make sure no friend is in the mine's radius. If there is any, explosion is delayed until he's at a safe distance
+ if(WEP_CVAR(minelayer, protection) && self.mine_explodeanyway == 0)
+ {
+ entity head;
+ head = findradius(self.origin, WEP_CVAR(minelayer, radius));
+ while(head)
+ {
- if(head != self.realowner && IsDifferentTeam(head, self.realowner)) // don't trigger for team mates
++ if(head == self.realowner || SAME_TEAM(head, self.realowner))
+ return;
+ head = head.chain;
+ }
+ }
+
+ self.mine_time = 0;
+ W_Mine_Explode();
+}
+
+float W_Mine_Count(entity e)
+{
+ float minecount = 0;
+ entity mine;
+ for(mine = world; (mine = find(mine, classname, "mine")); ) if(mine.realowner == e)
+ minecount += 1;
+
+ return minecount;
+}
+
+void W_Mine_Think (void)
+{
+ entity head;
+
+ self.nextthink = time;
+
+ if(self.movetype == MOVETYPE_FOLLOW)
+ {
+ if(LostMovetypeFollow(self))
+ {
+ UnsetMovetypeFollow(self);
+ self.movetype = MOVETYPE_NONE;
+ }
+ }
+
+ // our lifetime has expired, it's time to die - mine_time just allows us to play a sound for this
+ // TODO: replace this mine_trigger.wav sound with a real countdown
+ if ((time > self.cnt) && (!self.mine_time))
+ {
+ if(WEP_CVAR(minelayer, lifetime_countdown) > 0)
+ spamsound (self, CH_SHOTS, "weapons/mine_trigger.wav", VOL_BASE, ATTN_NORM);
+ self.mine_time = time + WEP_CVAR(minelayer, lifetime_countdown);
+ self.mine_explodeanyway = 1; // make the mine super aggressive -- Samual: Rather, make it not care if a team mate is near.
+ }
+
+ // a player's mines shall explode if he disconnects or dies
+ // TODO: Do this on team change too -- Samual: But isn't a player killed when they switch teams?
+ if(!IS_PLAYER(self.realowner) || self.realowner.deadflag != DEAD_NO)
+ {
+ other = world;
+ self.projectiledeathtype |= HITTYPE_BOUNCE;
+ W_Mine_Explode();
+ return;
+ }
+
+ // set the mine for detonation when a foe gets close enough
+ head = findradius(self.origin, WEP_CVAR(minelayer, proximityradius));
+ while(head)
+ {
+ if(IS_PLAYER(head) && head.deadflag == DEAD_NO)
++ if(head != self.realowner && DIFF_TEAM(head, self.realowner)) // don't trigger for team mates
+ if(!self.mine_time)
+ {
+ spamsound (self, CH_SHOTS, "weapons/mine_trigger.wav", VOL_BASE, ATTN_NORM);
+ self.mine_time = time + WEP_CVAR(minelayer, time);
+ }
+ head = head.chain;
+ }
+
+ // explode if it's time to
+ if(self.mine_time && time >= self.mine_time)
+ {
+ W_Mine_ProximityExplode();
+ return;
+ }
+
+ // remote detonation
+ if (self.realowner.weapon == WEP_MINE_LAYER)
+ if (self.realowner.deadflag == DEAD_NO)
+ if (self.minelayer_detonate)
+ W_Mine_RemoteExplode();
+}
+
+void W_Mine_Touch (void)
+{
+ if(self.movetype == MOVETYPE_NONE || self.movetype == MOVETYPE_FOLLOW)
+ return; // we're already a stuck mine, why do we get called? TODO does this even happen?
+
+ if(WarpZone_Projectile_Touch())
+ {
+ if(wasfreed(self))
+ self.realowner.minelayer_mines -= 1;
+ return;
+ }
+
+ if(other && IS_PLAYER(other) && other.deadflag == DEAD_NO)
+ {
+ // hit a player
+ // don't stick
+ }
+ else
+ {
+ W_Mine_Stick(other);
+ }
+}
+
+void W_Mine_Damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+{
+ if (self.health <= 0)
+ return;
+
+ float is_from_enemy = (inflictor.realowner != self.realowner);
+
+ if (!W_CheckProjectileDamage(inflictor.realowner, self.realowner, deathtype, (is_from_enemy ? 1 : -1)))
+ return; // g_projectiles_damage says to halt
+
+ self.health = self.health - damage;
+ self.angles = vectoangles(self.velocity);
+
+ if (self.health <= 0)
+ W_PrepareExplosionByDamage(attacker, W_Mine_Explode);
+}
+
+void W_Mine_Attack (void)
+{
+ entity mine;
+ entity flash;
+
+ // scan how many mines we placed, and return if we reached our limit
+ if(WEP_CVAR(minelayer, limit))
+ {
+ if(self.minelayer_mines >= WEP_CVAR(minelayer, limit))
+ {
+ // the refire delay keeps this message from being spammed
+ sprint(self, strcat("minelayer: You cannot place more than ^2", ftos(WEP_CVAR(minelayer, limit)), " ^7mines at a time\n") );
+ play2(self, "weapons/unavailable.wav");
+ return;
+ }
+ }
+
+ W_DecreaseAmmo(ammo_rockets, WEP_CVAR(minelayer, ammo), autocvar_g_balance_minelayer_reload_ammo);
+
+ W_SetupShot_ProjectileSize (self, '-4 -4 -4', '4 4 4', FALSE, 5, "weapons/mine_fire.wav", CH_WEAPON_A, WEP_CVAR(minelayer, damage));
+ pointparticles(particleeffectnum("rocketlauncher_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+ mine = WarpZone_RefSys_SpawnSameRefSys(self);
+ mine.owner = mine.realowner = self;
+ if(WEP_CVAR(minelayer, detonatedelay) >= 0)
+ mine.spawnshieldtime = time + WEP_CVAR(minelayer, detonatedelay);
+ else
+ mine.spawnshieldtime = -1;
+ mine.classname = "mine";
+ mine.bot_dodge = TRUE;
+ mine.bot_dodgerating = WEP_CVAR(minelayer, damage) * 2; // * 2 because it can detonate inflight which makes it even more dangerous
+
+ mine.takedamage = DAMAGE_YES;
+ mine.damageforcescale = WEP_CVAR(minelayer, damageforcescale);
+ mine.health = WEP_CVAR(minelayer, health);
+ mine.event_damage = W_Mine_Damage;
+ mine.damagedbycontents = TRUE;
+
+ mine.movetype = MOVETYPE_TOSS;
+ PROJECTILE_MAKETRIGGER(mine);
+ mine.projectiledeathtype = WEP_MINE_LAYER;
+ setsize (mine, '-4 -4 -4', '4 4 4'); // give it some size so it can be shot
+
+ setorigin (mine, w_shotorg - v_forward * 4); // move it back so it hits the wall at the right point
+ W_SetupProjectileVelocity(mine, WEP_CVAR(minelayer, speed), 0);
+ mine.angles = vectoangles (mine.velocity);
+
+ mine.touch = W_Mine_Touch;
+ mine.think = W_Mine_Think;
+ mine.nextthink = time;
+ mine.cnt = time + (WEP_CVAR(minelayer, lifetime) - WEP_CVAR(minelayer, lifetime_countdown));
+ mine.flags = FL_PROJECTILE;
+ mine.missile_flags = MIF_SPLASH | MIF_ARC | MIF_PROXY;
+
+ CSQCProjectile(mine, TRUE, PROJECTILE_MINE, TRUE);
+
+ // muzzle flash for 1st person view
+ flash = spawn ();
+ setmodel (flash, "models/flash.md3"); // precision set below
+ SUB_SetFade (flash, time, 0.1);
+ flash.effects = EF_ADDITIVE | EF_FULLBRIGHT | EF_LOWPRECISION;
+ W_AttachToShotorg(flash, '5 0 0');
+
+ // common properties
+
+ other = mine; MUTATOR_CALLHOOK(EditProjectile);
+
+ self.minelayer_mines = W_Mine_Count(self);
+}
+
+float W_PlacedMines(float detonate)
+{
+ entity mine;
+ float minfound = 0;
+
+ for(mine = world; (mine = find(mine, classname, "mine")); ) if(mine.realowner == self)
+ {
+ if(detonate)
+ {
+ if(!mine.minelayer_detonate)
+ {
+ mine.minelayer_detonate = TRUE;
+ minfound = 1;
+ }
+ }
+ else
+ minfound = 1;
+ }
+ return minfound;
+}
+
+float w_minelayer(float req)
+{
+ entity mine;
+ float ammo_amount;
+ switch(req)
+ {
+ case WR_AIM:
+ {
+ // aim and decide to fire if appropriate
+ if(self.minelayer_mines >= WEP_CVAR(minelayer, limit))
+ self.BUTTON_ATCK = FALSE;
+ else
+ self.BUTTON_ATCK = bot_aim(WEP_CVAR(minelayer, speed), 0, WEP_CVAR(minelayer, lifetime), FALSE);
+ if(skill >= 2) // skill 0 and 1 bots won't detonate mines!
+ {
+ // decide whether to detonate mines
+ entity targetlist, targ;
+ float edgedamage, coredamage, edgeradius, recipricoledgeradius, d;
+ float selfdamage, teamdamage, enemydamage;
+ edgedamage = WEP_CVAR(minelayer, edgedamage);
+ coredamage = WEP_CVAR(minelayer, damage);
+ edgeradius = WEP_CVAR(minelayer, radius);
+ recipricoledgeradius = 1 / edgeradius;
+ selfdamage = 0;
+ teamdamage = 0;
+ enemydamage = 0;
+ targetlist = findchainfloat(bot_attack, TRUE);
+ mine = find(world, classname, "mine");
+ while (mine)
+ {
+ if (mine.realowner != self)
+ {
+ mine = find(mine, classname, "mine");
+ continue;
+ }
+ targ = targetlist;
+ while (targ)
+ {
+ d = vlen(targ.origin + (targ.mins + targ.maxs) * 0.5 - mine.origin);
+ d = bound(0, edgedamage + (coredamage - edgedamage) * sqrt(1 - d * recipricoledgeradius), 10000);
+ // count potential damage according to type of target
+ if (targ == self)
+ selfdamage = selfdamage + d;
+ else if (targ.team == self.team && teamplay)
+ teamdamage = teamdamage + d;
+ else if (bot_shouldattack(targ))
+ enemydamage = enemydamage + d;
+ targ = targ.chain;
+ }
+ mine = find(mine, classname, "mine");
+ }
+ float desirabledamage;
+ desirabledamage = enemydamage;
+ if (time > self.invincible_finished && time > self.spawnshieldtime)
+ desirabledamage = desirabledamage - selfdamage * autocvar_g_balance_selfdamagepercent;
+ if (teamplay && self.team)
+ desirabledamage = desirabledamage - teamdamage;
+
+ mine = find(world, classname, "mine");
+ while (mine)
+ {
+ if (mine.realowner != self)
+ {
+ mine = find(mine, classname, "mine");
+ continue;
+ }
+ makevectors(mine.v_angle);
+ targ = targetlist;
+ if (skill > 9) // normal players only do this for the target they are tracking
+ {
+ targ = targetlist;
+ while (targ)
+ {
+ if (
+ (v_forward * normalize(mine.origin - targ.origin)< 0.1)
+ && desirabledamage > 0.1*coredamage
+ )self.BUTTON_ATCK2 = TRUE;
+ targ = targ.chain;
+ }
+ }else{
+ float distance; distance= bound(300,vlen(self.origin-self.enemy.origin),30000);
+ //As the distance gets larger, a correct detonation gets near imposible
+ //Bots are assumed to use the mine spawnfunc_light to see if the mine gets near a player
+ if(v_forward * normalize(mine.origin - self.enemy.origin)< 0.1)
+ if(IS_PLAYER(self.enemy))
+ if(desirabledamage >= 0.1*coredamage)
+ if(random()/distance*300 > frametime*bound(0,(10-skill)*0.2,1))
+ self.BUTTON_ATCK2 = TRUE;
+ // dprint(ftos(random()/distance*300),">");dprint(ftos(frametime*bound(0,(10-skill)*0.2,1)),"\n");
+ }
+
+ mine = find(mine, classname, "mine");
+ }
+ // if we would be doing at X percent of the core damage, detonate it
+ // but don't fire a new shot at the same time!
+ if (desirabledamage >= 0.75 * coredamage) //this should do group damage in rare fortunate events
+ self.BUTTON_ATCK2 = TRUE;
+ if ((skill > 6.5) && (selfdamage > self.health))
+ self.BUTTON_ATCK2 = FALSE;
+ //if(self.BUTTON_ATCK2 == TRUE)
+ // dprint(ftos(desirabledamage),"\n");
+ if (self.BUTTON_ATCK2 == TRUE) self.BUTTON_ATCK = FALSE;
+ }
+
+ return TRUE;
+ }
+ case WR_THINK:
+ {
+ if(autocvar_g_balance_minelayer_reload_ammo && self.clip_load < WEP_CVAR(minelayer, ammo)) // forced reload
+ {
+ // not if we're holding the minelayer without enough ammo, but can detonate existing mines
+ if not (W_PlacedMines(FALSE) && self.ammo_rockets < WEP_CVAR(minelayer, ammo))
+ WEP_ACTION(self.weapon, WR_RELOAD);
+ }
+ else if (self.BUTTON_ATCK)
+ {
+ if(weapon_prepareattack(0, WEP_CVAR(minelayer, refire)))
+ {
+ W_Mine_Attack();
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR(minelayer, animtime), w_ready);
+ }
+ }
+
+ if (self.BUTTON_ATCK2)
+ {
+ if(W_PlacedMines(TRUE))
+ sound (self, CH_WEAPON_B, "weapons/mine_det.wav", VOL_BASE, ATTN_NORM);
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_model ("models/flash.md3");
+ precache_model ("models/mine.md3");
+ precache_model ("models/weapons/g_minelayer.md3");
+ precache_model ("models/weapons/v_minelayer.md3");
+ precache_model ("models/weapons/h_minelayer.iqm");
+ precache_sound ("weapons/mine_det.wav");
+ precache_sound ("weapons/mine_fire.wav");
+ precache_sound ("weapons/mine_stick.wav");
+ precache_sound ("weapons/mine_trigger.wav");
+ WEP_SET_PROPS(MINELAYER_SETTINGS(minelayer), WEP_MINE_LAYER)
+ return TRUE;
+ }
+ case WR_SETUP:
+ {
+ self.current_ammo = ammo_rockets;
+ return TRUE;
+ }
+ case WR_CHECKAMMO1:
+ {
+ // don't switch while placing a mine
+ if (ATTACK_FINISHED(self) <= time || self.weapon != WEP_MINE_LAYER)
+ {
+ ammo_amount = self.ammo_rockets >= WEP_CVAR(minelayer, ammo);
+ ammo_amount += self.(weapon_load[WEP_MINE_LAYER]) >= WEP_CVAR(minelayer, ammo);
+ return ammo_amount;
+ }
+ return TRUE;
+ }
+ case WR_CHECKAMMO2:
+ {
+ if (W_PlacedMines(FALSE))
+ return TRUE;
+ else
+ return FALSE;
+ }
+ case WR_CONFIG:
+ {
+ WEP_CONFIG_SETTINGS(MINELAYER_SETTINGS(minelayer))
+ return TRUE;
+ }
+ case WR_RESETPLAYER:
+ {
+ self.minelayer_mines = 0;
+ return TRUE;
+ }
+ case WR_RELOAD:
+ {
+ W_Reload(WEP_CVAR(minelayer, ammo), "weapons/reload.wav");
+ return TRUE;
+ }
+ case WR_SUICIDEMESSAGE:
+ {
+ return WEAPON_MINELAYER_SUICIDE;
+ }
+ case WR_KILLMESSAGE:
+ {
+ return WEAPON_MINELAYER_MURDER;
+ }
+ }
+ return TRUE;
+}
+#endif
+#ifdef CSQC
+float w_minelayer(float req)
+{
+ switch(req)
+ {
+ case WR_IMPACTEFFECT:
+ {
+ vector org2;
+ org2 = w_org + w_backoff * 12;
+ pointparticles(particleeffectnum("rocket_explode"), org2, '0 0 0', 1);
+ if(!w_issilent)
+ sound(self, CH_SHOTS, "weapons/mine_exp.wav", VOL_BASE, ATTN_NORM);
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_sound("weapons/mine_exp.wav");
+ return TRUE;
+ }
+ }
+ return TRUE;
+}
+#endif
+#endif
--- /dev/null
- if(IsDifferentTeam(self.realowner, other))
+#ifdef REGISTER_WEAPON
+REGISTER_WEAPON(
+/* WEP_##id */ GRENADE_LAUNCHER,
+/* function */ w_glauncher,
+/* ammotype */ IT_ROCKETS,
+/* impulse */ 4,
+/* flags */ WEP_FLAG_NORMAL | WEP_FLAG_RELOADABLE | WEP_FLAG_CANCLIMB | WEP_TYPE_SPLASH,
+/* rating */ BOT_PICKUP_RATING_MID,
+/* model */ "gl",
+/* netname */ "grenadelauncher",
+/* fullname */ _("Mortar")
+);
+
+#define MORTAR_SETTINGS(weapon) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, ammo) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, animtime) \
+ WEP_ADD_CVAR(weapon, MO_NONE, bouncefactor) \
+ WEP_ADD_CVAR(weapon, MO_NONE, bouncestop) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, damage) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, damageforcescale) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, edgedamage) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, force) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, health) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, lifetime) \
+ WEP_ADD_CVAR(weapon, MO_SEC, lifetime_bounce) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, lifetime_stick) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, radius) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, refire) \
+ WEP_ADD_CVAR(weapon, MO_PRI, remote_minbouncecnt) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, speed) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, speed_up) \
+ WEP_ADD_CVAR(weapon, MO_BOTH, type) \
+ WEP_ADD_PROP(weapon, reloading_ammo, reload_ammo) \
+ WEP_ADD_PROP(weapon, reloading_time, reload_time) \
+ WEP_ADD_PROP(weapon, switchdelay_raise, switchdelay_raise) \
+ WEP_ADD_PROP(weapon, switchdelay_drop, switchdelay_drop)
+
+#ifdef SVQC
+MORTAR_SETTINGS(mortar)
+.float gl_detonate_later;
+.float gl_bouncecnt;
+#endif
+#else
+#ifdef SVQC
+
+void W_Grenade_Explode (void)
+{
+ if(other.takedamage == DAMAGE_AIM)
+ if(IS_PLAYER(other))
++ if(DIFF_TEAM(self.realowner, other))
+ if(other.deadflag == DEAD_NO)
+ if(IsFlying(other))
+ Send_Notification(NOTIF_ONE, self.realowner, MSG_ANNCE, ANNCE_ACHIEVEMENT_AIRSHOT);
+
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+
+ if(self.movetype == MOVETYPE_NONE)
+ self.velocity = self.oldvelocity;
+
+ RadiusDamage (self, self.realowner, WEP_CVAR_PRI(mortar, damage), WEP_CVAR_PRI(mortar, edgedamage), WEP_CVAR_PRI(mortar, radius), world, world, WEP_CVAR_PRI(mortar, force), self.projectiledeathtype, other);
+
+ remove (self);
+}
+
+void W_Grenade_Explode2 (void)
+{
+ if(other.takedamage == DAMAGE_AIM)
+ if(IS_PLAYER(other))
+ if(IsDifferentTeam(self.realowner, other))
+ if(other.deadflag == DEAD_NO)
+ if(IsFlying(other))
+ Send_Notification(NOTIF_ONE, self.realowner, MSG_ANNCE, ANNCE_ACHIEVEMENT_AIRSHOT);
+
+ self.event_damage = func_null;
+ self.takedamage = DAMAGE_NO;
+
+ if(self.movetype == MOVETYPE_NONE)
+ self.velocity = self.oldvelocity;
+
+ RadiusDamage (self, self.realowner, WEP_CVAR_SEC(mortar, damage), WEP_CVAR_SEC(mortar, edgedamage), WEP_CVAR_SEC(mortar, radius), world, world, WEP_CVAR_SEC(mortar, force), self.projectiledeathtype, other);
+
+ remove (self);
+}
+
+void W_Grenade_Damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+{
+ if (self.health <= 0)
+ return;
+
+ if (!W_CheckProjectileDamage(inflictor.realowner, self.realowner, deathtype, -1)) // no exceptions
+ return; // g_projectiles_damage says to halt
+
+ self.health = self.health - damage;
+
+ if (self.health <= 0)
+ W_PrepareExplosionByDamage(attacker, self.use);
+}
+
+void W_Grenade_Think1 (void)
+{
+ self.nextthink = time;
+ if (time > self.cnt)
+ {
+ other = world;
+ self.projectiledeathtype |= HITTYPE_BOUNCE;
+ W_Grenade_Explode ();
+ return;
+ }
+ if(self.gl_detonate_later && self.gl_bouncecnt >= WEP_CVAR_PRI(mortar, remote_minbouncecnt))
+ W_Grenade_Explode();
+}
+
+void W_Grenade_Touch1 (void)
+{
+ PROJECTILE_TOUCH;
+ if (other.takedamage == DAMAGE_AIM || WEP_CVAR_PRI(mortar, type) == 0) // always explode when hitting a player, or if normal mortar projectile
+ {
+ self.use ();
+ }
+ else if (WEP_CVAR_PRI(mortar, type) == 1) // bounce
+ {
+ float r;
+ r = random() * 6;
+ if(r < 1)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce1.wav", VOL_BASE, ATTN_NORM);
+ else if(r < 2)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce2.wav", VOL_BASE, ATTN_NORM);
+ else if(r < 3)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce3.wav", VOL_BASE, ATTN_NORM);
+ else if(r < 4)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce4.wav", VOL_BASE, ATTN_NORM);
+ else if(r < 5)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce5.wav", VOL_BASE, ATTN_NORM);
+ else
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce6.wav", VOL_BASE, ATTN_NORM);
+ self.projectiledeathtype |= HITTYPE_BOUNCE;
+ self.gl_bouncecnt += 1;
+ }
+ else if(WEP_CVAR_PRI(mortar, type) == 2 && (!other || (other.takedamage != DAMAGE_AIM && other.movetype == MOVETYPE_NONE))) // stick
+ {
+ spamsound (self, CH_SHOTS, "weapons/grenade_stick.wav", VOL_BASE, ATTN_NORM);
+
+ // let it stick whereever it is
+ self.oldvelocity = self.velocity;
+ self.velocity = '0 0 0';
+ self.movetype = MOVETYPE_NONE; // also disables gravity
+ self.gravity = 0; // nope, it does NOT! maybe a bug in CSQC code? TODO
+ UpdateCSQCProjectile(self);
+
+ // do not respond to any more touches
+ self.solid = SOLID_NOT;
+
+ self.nextthink = min(self.nextthink, time + WEP_CVAR_PRI(mortar, lifetime_stick));
+ }
+}
+
+void W_Grenade_Touch2 (void)
+{
+ PROJECTILE_TOUCH;
+ if (other.takedamage == DAMAGE_AIM || WEP_CVAR_SEC(mortar, type) == 0) // always explode when hitting a player, or if normal mortar projectile
+ {
+ self.use ();
+ }
+ else if (WEP_CVAR_SEC(mortar, type) == 1) // bounce
+ {
+ float r;
+ r = random() * 6;
+ if(r < 1)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce1.wav", VOL_BASE, ATTN_NORM);
+ else if(r < 2)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce2.wav", VOL_BASE, ATTN_NORM);
+ else if(r < 3)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce3.wav", VOL_BASE, ATTN_NORM);
+ else if(r < 4)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce4.wav", VOL_BASE, ATTN_NORM);
+ else if(r < 5)
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce5.wav", VOL_BASE, ATTN_NORM);
+ else
+ spamsound (self, CH_SHOTS, "weapons/grenade_bounce6.wav", VOL_BASE, ATTN_NORM);
+ self.projectiledeathtype |= HITTYPE_BOUNCE;
+ self.gl_bouncecnt += 1;
+
+ if (WEP_CVAR_SEC(mortar, lifetime_bounce) && self.gl_bouncecnt == 1)
+ self.nextthink = time + WEP_CVAR_SEC(mortar, lifetime_bounce);
+
+ }
+ else if(WEP_CVAR_SEC(mortar, type) == 2 && (!other || (other.takedamage != DAMAGE_AIM && other.movetype == MOVETYPE_NONE))) // stick
+ {
+ spamsound (self, CH_SHOTS, "weapons/grenade_stick.wav", VOL_BASE, ATTN_NORM);
+
+ // let it stick whereever it is
+ self.oldvelocity = self.velocity;
+ self.velocity = '0 0 0';
+ self.movetype = MOVETYPE_NONE; // also disables gravity
+ self.gravity = 0; // nope, it does NOT! maybe a bug in CSQC code? TODO
+ UpdateCSQCProjectile(self);
+
+ // do not respond to any more touches
+ self.solid = SOLID_NOT;
+
+ self.nextthink = min(self.nextthink, time + WEP_CVAR_SEC(mortar, lifetime_stick));
+ }
+}
+
+void W_Grenade_Attack (void)
+{
+ entity gren;
+
+ W_DecreaseAmmo(ammo_rockets, WEP_CVAR_PRI(mortar, ammo), autocvar_g_balance_mortar_reload_ammo); // WEAPONTODO
+
+ W_SetupShot_ProjectileSize (self, '-3 -3 -3', '3 3 3', FALSE, 4, "weapons/grenade_fire.wav", CH_WEAPON_A, WEP_CVAR_PRI(mortar, damage));
+ w_shotdir = v_forward; // no TrueAim for grenades please
+
+ pointparticles(particleeffectnum("grenadelauncher_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+ gren = spawn ();
+ gren.owner = gren.realowner = self;
+ gren.classname = "grenade";
+ gren.bot_dodge = TRUE;
+ gren.bot_dodgerating = WEP_CVAR_PRI(mortar, damage);
+ gren.movetype = MOVETYPE_BOUNCE;
+ gren.bouncefactor = WEP_CVAR(mortar, bouncefactor);
+ gren.bouncestop = WEP_CVAR(mortar, bouncestop);
+ PROJECTILE_MAKETRIGGER(gren);
+ gren.projectiledeathtype = WEP_GRENADE_LAUNCHER;
+ setorigin(gren, w_shotorg);
+ setsize(gren, '-3 -3 -3', '3 3 3');
+
+ gren.cnt = time + WEP_CVAR_PRI(mortar, lifetime);
+ gren.nextthink = time;
+ gren.think = W_Grenade_Think1;
+ gren.use = W_Grenade_Explode;
+ gren.touch = W_Grenade_Touch1;
+
+ gren.takedamage = DAMAGE_YES;
+ gren.health = WEP_CVAR_PRI(mortar, health);
+ gren.damageforcescale = WEP_CVAR_PRI(mortar, damageforcescale);
+ gren.event_damage = W_Grenade_Damage;
+ gren.damagedbycontents = TRUE;
+ gren.missile_flags = MIF_SPLASH | MIF_ARC;
+ W_SETUPPROJECTILEVELOCITY_UP(gren, g_balance_mortar_primary); // WEAPONTODO
+
+ gren.angles = vectoangles (gren.velocity);
+ gren.flags = FL_PROJECTILE;
+
+ if(WEP_CVAR_PRI(mortar, type) == 0 || WEP_CVAR_PRI(mortar, type) == 2)
+ CSQCProjectile(gren, TRUE, PROJECTILE_GRENADE, TRUE);
+ else
+ CSQCProjectile(gren, TRUE, PROJECTILE_GRENADE_BOUNCING, TRUE);
+
+ other = gren; MUTATOR_CALLHOOK(EditProjectile);
+}
+
+void W_Grenade_Attack2 (void)
+{
+ entity gren;
+
+ W_DecreaseAmmo(ammo_rockets, WEP_CVAR_SEC(mortar, ammo), autocvar_g_balance_mortar_reload_ammo);
+
+ W_SetupShot_ProjectileSize (self, '-3 -3 -3', '3 3 3', FALSE, 4, "weapons/grenade_fire.wav", CH_WEAPON_A, WEP_CVAR_SEC(mortar, damage));
+ w_shotdir = v_forward; // no TrueAim for grenades please
+
+ pointparticles(particleeffectnum("grenadelauncher_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+ gren = spawn ();
+ gren.owner = gren.realowner = self;
+ gren.classname = "grenade";
+ gren.bot_dodge = TRUE;
+ gren.bot_dodgerating = WEP_CVAR_SEC(mortar, damage);
+ gren.movetype = MOVETYPE_BOUNCE;
+ gren.bouncefactor = WEP_CVAR(mortar, bouncefactor);
+ gren.bouncestop = WEP_CVAR(mortar, bouncestop);
+ PROJECTILE_MAKETRIGGER(gren);
+ gren.projectiledeathtype = WEP_GRENADE_LAUNCHER | HITTYPE_SECONDARY;
+ setorigin(gren, w_shotorg);
+ setsize(gren, '-3 -3 -3', '3 3 3');
+
+ gren.nextthink = time + WEP_CVAR_SEC(mortar, lifetime);
+ gren.think = adaptor_think2use_hittype_splash;
+ gren.use = W_Grenade_Explode2;
+ gren.touch = W_Grenade_Touch2;
+
+ gren.takedamage = DAMAGE_YES;
+ gren.health = WEP_CVAR_SEC(mortar, health);
+ gren.damageforcescale = WEP_CVAR_SEC(mortar, damageforcescale);
+ gren.event_damage = W_Grenade_Damage;
+ gren.damagedbycontents = TRUE;
+ gren.missile_flags = MIF_SPLASH | MIF_ARC;
+ W_SETUPPROJECTILEVELOCITY_UP(gren, g_balance_mortar_secondary); // WEAPONTODO
+
+ gren.angles = vectoangles (gren.velocity);
+ gren.flags = FL_PROJECTILE;
+
+ if(WEP_CVAR_SEC(mortar, type) == 0 || WEP_CVAR_SEC(mortar, type) == 2)
+ CSQCProjectile(gren, TRUE, PROJECTILE_GRENADE, TRUE);
+ else
+ CSQCProjectile(gren, TRUE, PROJECTILE_GRENADE_BOUNCING, TRUE);
+
+ other = gren; MUTATOR_CALLHOOK(EditProjectile);
+}
+
+void spawnfunc_weapon_grenadelauncher (void)
+{
+ weapon_defaultspawnfunc(WEP_GRENADE_LAUNCHER);
+}
+
+.float bot_secondary_grenademooth;
+float w_glauncher(float req)
+{
+ entity nade;
+ float nadefound;
+ float ammo_amount;
+ switch(req)
+ {
+ case WR_AIM:
+ {
+ self.BUTTON_ATCK = FALSE;
+ self.BUTTON_ATCK2 = FALSE;
+ if (self.bot_secondary_grenademooth == 0) // WEAPONTODO: merge this into using WEP_CVAR_BOTH
+ {
+ if(bot_aim(WEP_CVAR_PRI(mortar, speed), WEP_CVAR_PRI(mortar, speed_up), WEP_CVAR_PRI(mortar, lifetime), TRUE))
+ {
+ self.BUTTON_ATCK = TRUE;
+ if(random() < 0.01) self.bot_secondary_grenademooth = 1;
+ }
+ }
+ else
+ {
+ if(bot_aim(WEP_CVAR_SEC(mortar, speed), WEP_CVAR_SEC(mortar, speed_up), WEP_CVAR_SEC(mortar, lifetime), TRUE))
+ {
+ self.BUTTON_ATCK2 = TRUE;
+ if(random() < 0.02) self.bot_secondary_grenademooth = 0;
+ }
+ }
+
+ return TRUE;
+ }
+ /*case WR_CALCINFO:
+ {
+ wepinfo_pri_refire = max3(sys_frametime, WEP_CVAR_PRI(mortar, refire), WEP_CVAR_PRI(mortar, animtime));
+ wepinfo_pri_dps = (WEP_CVAR_PRI(mortar, damage) * (1 / wepinfo_pri_refire));
+ wepinfo_pri_speed = (1 / max(1, (10000 / max(1, WEP_CVAR_PRI(mortar, speed)))));
+
+ // for the range calculation, closer to 1 is better
+ wepinfo_pri_range_max = 2000 * wepinfo_pri_speed;
+ wepinfo_pri_range = wepinfo_pri_speed * WEP_CVAR_PRI(mortar,
+
+ wepinfo_sec_refire = max3(sys_frametime, WEP_CVAR_SEC(mortar, refire), WEP_CVAR_SEC(mortar, animtime));
+ wepinfo_sec_dps = (WEP_CVAR_SEC(mortar, damage) * (1 / wepinfo_sec_refire));
+
+ wepinfo_sec_dps = (WEP_CVAR_SEC(mortar, damage) * (1 / max3(sys_frametime, WEP_CVAR_SEC(mortar, refire), WEP_CVAR_SEC(mortar, animtime))));
+ wepinfo_ter_dps = 0;
+ */
+ case WR_THINK:
+ {
+ if(autocvar_g_balance_mortar_reload_ammo && self.clip_load < min(WEP_CVAR_PRI(mortar, ammo), WEP_CVAR_SEC(mortar, ammo))) // forced reload
+ WEP_ACTION(self.weapon, WR_RELOAD);
+ else if (self.BUTTON_ATCK)
+ {
+ if (weapon_prepareattack(0, WEP_CVAR_PRI(mortar, refire)))
+ {
+ W_Grenade_Attack();
+ weapon_thinkf(WFRAME_FIRE1, WEP_CVAR_PRI(mortar, animtime), w_ready);
+ }
+ }
+ else if (self.BUTTON_ATCK2)
+ {
+ if (cvar("g_balance_mortar_secondary_remote_detonateprimary"))
+ {
+ nadefound = 0;
+ for(nade = world; (nade = find(nade, classname, "grenade")); ) if(nade.realowner == self)
+ {
+ if(!nade.gl_detonate_later)
+ {
+ nade.gl_detonate_later = TRUE;
+ nadefound = 1;
+ }
+ }
+ if(nadefound)
+ sound (self, CH_WEAPON_B, "weapons/rocket_det.wav", VOL_BASE, ATTN_NORM);
+ }
+ else if (weapon_prepareattack(1, WEP_CVAR_SEC(mortar, refire)))
+ {
+ W_Grenade_Attack2();
+ weapon_thinkf(WFRAME_FIRE2, WEP_CVAR_SEC(mortar, animtime), w_ready);
+ }
+ }
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_model ("models/weapons/g_gl.md3");
+ precache_model ("models/weapons/v_gl.md3");
+ precache_model ("models/weapons/h_gl.iqm");
+ precache_sound ("weapons/grenade_bounce1.wav");
+ precache_sound ("weapons/grenade_bounce2.wav");
+ precache_sound ("weapons/grenade_bounce3.wav");
+ precache_sound ("weapons/grenade_bounce4.wav");
+ precache_sound ("weapons/grenade_bounce5.wav");
+ precache_sound ("weapons/grenade_bounce6.wav");
+ precache_sound ("weapons/grenade_stick.wav");
+ precache_sound ("weapons/grenade_fire.wav");
+ WEP_SET_PROPS(MORTAR_SETTINGS(mortar), WEP_GRENADE_LAUNCHER)
+ return TRUE;
+ }
+ case WR_SETUP:
+ {
+ self.current_ammo = ammo_rockets;
+ return TRUE;
+ }
+ case WR_CHECKAMMO1:
+ {
+ ammo_amount = self.ammo_rockets >= WEP_CVAR_PRI(mortar, ammo);
+ ammo_amount += self.(weapon_load[WEP_GRENADE_LAUNCHER]) >= WEP_CVAR_PRI(mortar, ammo);
+ return ammo_amount;
+ }
+ case WR_CHECKAMMO2:
+ {
+ ammo_amount = self.ammo_rockets >= WEP_CVAR_SEC(mortar, ammo);
+ ammo_amount += self.(weapon_load[WEP_GRENADE_LAUNCHER]) >= WEP_CVAR_SEC(mortar, ammo);
+ return ammo_amount;
+ }
+ case WR_CONFIG:
+ {
+ WEP_CONFIG_SETTINGS(MORTAR_SETTINGS(mortar))
+ return TRUE;
+ }
+ case WR_RELOAD:
+ {
+ W_Reload(min(WEP_CVAR_PRI(mortar, ammo), WEP_CVAR_SEC(mortar, ammo)), "weapons/reload.wav"); // WEAPONTODO
+ return TRUE;
+ }
+ case WR_SUICIDEMESSAGE:
+ {
+ if(w_deathtype & HITTYPE_SECONDARY)
+ return WEAPON_MORTAR_SUICIDE_BOUNCE;
+ else
+ return WEAPON_MORTAR_SUICIDE_EXPLODE;
+ }
+ case WR_KILLMESSAGE:
+ {
+ if(w_deathtype & HITTYPE_SECONDARY)
+ return WEAPON_MORTAR_MURDER_BOUNCE;
+ else
+ return WEAPON_MORTAR_MURDER_EXPLODE;
+ }
+ }
+ return TRUE;
+}
+#endif
+#ifdef CSQC
+float w_glauncher(float req)
+{
+ switch(req)
+ {
+ case WR_IMPACTEFFECT:
+ {
+ vector org2;
+ org2 = w_org + w_backoff * 12;
+ pointparticles(particleeffectnum("grenade_explode"), org2, '0 0 0', 1);
+ if(!w_issilent)
+ sound(self, CH_SHOTS, "weapons/grenade_impact.wav", VOL_BASE, ATTN_NORM);
+
+ return TRUE;
+ }
+ case WR_INIT:
+ {
+ precache_sound("weapons/grenade_impact.wav");
+ return TRUE;
+ }
+ }
+ return TRUE;
+}
+#endif
+#endif
--- /dev/null
- if(IsDifferentTeam(attacker, targ))
+float accuracy_byte(float n, float d)
+{
+ //print(sprintf("accuracy: %d / %d\n", n, d));
+ if(n <= 0)
+ return 0;
+ if(n > d)
+ return 255;
+ return 1 + rint(n * 100.0 / d);
+}
+
+float accuracy_send(entity to, float sf)
+{
+ float w, f;
+ entity a;
+ WriteByte(MSG_ENTITY, ENT_CLIENT_ACCURACY);
+
+ a = self.owner;
+ if(IS_SPEC(a))
+ a = a.enemy;
+ a = a.accuracy;
+
+ if(to != a.owner)
+ if not(self.owner.cvar_cl_accuracy_data_share && autocvar_sv_accuracy_data_share)
+ sf = 0;
+ // note: zero sendflags can never be sent... so we can use that to say that we send no accuracy!
+ WriteInt24_t(MSG_ENTITY, sf);
+ if(sf == 0)
+ return TRUE;
+ // note: we know that client and server agree about SendFlags...
+ for(w = 0, f = 1; w <= WEP_LAST - WEP_FIRST; ++w)
+ {
+ if(sf & f)
+ WriteByte(MSG_ENTITY, accuracy_byte(self.(accuracy_hit[w]), self.(accuracy_fired[w])));
+ if(f == 0x800000)
+ f = 1;
+ else
+ f *= 2;
+ }
+ return TRUE;
+}
+
+// init/free
+void accuracy_init(entity e)
+{
+ e.accuracy = spawn();
+ e.accuracy.owner = e;
+ e.accuracy.classname = "accuracy";
+ e.accuracy.drawonlytoclient = e;
+ Net_LinkEntity(e.accuracy, FALSE, 0, accuracy_send);
+}
+
+void accuracy_free(entity e)
+{
+ remove(e.accuracy);
+}
+
+// force a resend of a player's accuracy stats
+void accuracy_resend(entity e)
+{
+ e.accuracy.SendFlags = 0xFFFFFF;
+}
+
+// update accuracy stats
+.float hit_time;
+.float fired_time;
+
+void accuracy_add(entity e, float w, float fired, float hit)
+{
+ entity a;
+ float b;
+ if(IS_INDEPENDENT_PLAYER(e))
+ return;
+ a = e.accuracy;
+ if(!a || !(hit || fired))
+ return;
+ w -= WEP_FIRST;
+ b = accuracy_byte(a.(accuracy_hit[w]), a.(accuracy_fired[w]));
+ if(hit)
+ a.(accuracy_hit[w]) += hit;
+ if(fired)
+ a.(accuracy_fired[w]) += fired;
+
+ if(hit && a.hit_time != time) // only run this once per frame
+ {
+ a.(accuracy_cnt_hit[w]) += 1;
+ a.hit_time = time;
+ }
+
+ if(fired && a.fired_time != time) // only run this once per frame
+ {
+ a.(accuracy_cnt_fired[w]) += 1;
+ a.fired_time = time;
+ }
+
+ if(b == accuracy_byte(a.(accuracy_hit[w]), a.(accuracy_fired[w])))
+ return;
+ w = pow(2, mod(w, 24));
+ a.SendFlags |= w;
+ FOR_EACH_CLIENT(a)
+ if(IS_SPEC(a))
+ if(a.enemy == e)
+ a.SendFlags |= w;
+}
+
+float accuracy_isgooddamage(entity attacker, entity targ)
+{
+ if(!warmup_stage)
+ if(IS_CLIENT(targ))
+ if(targ.deadflag == DEAD_NO)
++ if(DIFF_TEAM(attacker, targ))
+ return TRUE;
+ return FALSE;
+}
+
+float accuracy_canbegooddamage(entity attacker)
+{
+ if(!warmup_stage)
+ return TRUE;
+ return FALSE;
+}