From: Samual Lenks Date: Sat, 14 Sep 2013 04:08:09 +0000 (-0400) Subject: Merge remote-tracking branch 'origin/master' into samual/weapons X-Git-Tag: xonotic-v0.8.0~152^2~323 X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=7ed6b5adc348af2a7094c85517e824c04e2005cd;p=xonotic%2Fxonotic-data.pk3dir.git Merge remote-tracking branch 'origin/master' into samual/weapons Conflicts: qcsrc/common/weapons/w_fireball.qc qcsrc/server/w_crylink.qc qcsrc/server/w_grenadelauncher.qc qcsrc/server/w_minelayer.qc qcsrc/server/w_rocketlauncher.qc --- 7ed6b5adc348af2a7094c85517e824c04e2005cd diff --cc qcsrc/common/weapons/w_crylink.qc index 12320a850,000000000..4e46f1fb2 mode 100644,000000..100644 --- a/qcsrc/common/weapons/w_crylink.qc +++ b/qcsrc/common/weapons/w_crylink.qc @@@ -1,712 -1,0 +1,712 @@@ +#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(IsDifferentTeam(head, projectile.realowner)) - ++hit_enemy; - else ++ 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 diff --cc qcsrc/common/weapons/w_devastator.qc index 46f4384dd,000000000..b764d942d mode 100644,000000..100644 --- a/qcsrc/common/weapons/w_devastator.qc +++ b/qcsrc/common/weapons/w_devastator.qc @@@ -1,551 -1,0 +1,551 @@@ +#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(IsDifferentTeam(self.realowner, 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 diff --cc qcsrc/common/weapons/w_electro.qc index 2b79a92b2,000000000..55843fcae mode 100644,000000..100644 --- a/qcsrc/common/weapons/w_electro.qc +++ b/qcsrc/common/weapons/w_electro.qc @@@ -1,638 -1,0 +1,638 @@@ +#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(IsDifferentTeam(self.realowner, 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 diff --cc qcsrc/common/weapons/w_fireball.qc index 83fe87ad2,000000000..466158441 mode 100644,000000..100644 --- a/qcsrc/common/weapons/w_fireball.qc +++ b/qcsrc/common/weapons/w_fireball.qc @@@ -1,471 -1,0 +1,471 @@@ +#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 || IsDifferentTeam(e, self)) ++ 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 diff --cc qcsrc/common/weapons/w_minelayer.qc index 06ff4ee0a,000000000..8322abeb4 mode 100644,000000..100644 --- a/qcsrc/common/weapons/w_minelayer.qc +++ b/qcsrc/common/weapons/w_minelayer.qc @@@ -1,611 -1,0 +1,611 @@@ +#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(IsDifferentTeam(self.realowner, 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(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)) ++ 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 && IsDifferentTeam(head, self.realowner)) // don't trigger for team mates ++ 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 diff --cc qcsrc/common/weapons/w_mortar.qc index 712944ec0,000000000..b6f156dfc mode 100644,000000..100644 --- a/qcsrc/common/weapons/w_mortar.qc +++ b/qcsrc/common/weapons/w_mortar.qc @@@ -1,477 -1,0 +1,477 @@@ +#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(IsDifferentTeam(self.realowner, 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 diff --cc qcsrc/server/weapons/accuracy.qc index 2429b2cb5,000000000..084a9d0e4 mode 100644,000000..100644 --- a/qcsrc/server/weapons/accuracy.qc +++ b/qcsrc/server/weapons/accuracy.qc @@@ -1,120 -1,0 +1,120 @@@ +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(IsDifferentTeam(attacker, targ)) ++ if(DIFF_TEAM(attacker, targ)) + return TRUE; + return FALSE; +} + +float accuracy_canbegooddamage(entity attacker) +{ + if(!warmup_stage) + return TRUE; + return FALSE; +}