3 /* ammotype */ ATTRIB(Mortar, ammo_field, .int, ammo_rockets)
4 /* impulse */ ATTRIB(Mortar, impulse, int, 4)
5 /* flags */ ATTRIB(Mortar, spawnflags, int, WEP_FLAG_NORMAL | WEP_FLAG_RELOADABLE | WEP_FLAG_CANCLIMB | WEP_TYPE_SPLASH);
6 /* rating */ ATTRIB(Mortar, bot_pickupbasevalue, float, BOT_PICKUP_RATING_MID);
7 /* color */ ATTRIB(Mortar, wpcolor, vector, '1 0 0');
8 /* modelname */ ATTRIB(Mortar, mdl, string, "gl");
10 /* model */ ATTRIB(Mortar, m_model, Model, MDL_MORTAR_ITEM);
12 /* crosshair */ ATTRIB(Mortar, w_crosshair, string, "gfx/crosshairgrenadelauncher");
13 /* crosshair */ ATTRIB(Mortar, w_crosshair_size, float, 0.7);
14 /* wepimg */ ATTRIB(Mortar, model2, string, "weapongrenadelauncher");
15 /* refname */ ATTRIB(Mortar, netname, string, "mortar");
16 /* wepname */ ATTRIB(Mortar, m_name, string, _("Mortar"));
18 #define X(BEGIN, P, END, class, prefix) \
20 P(class, prefix, ammo, float, BOTH) \
21 P(class, prefix, animtime, float, BOTH) \
22 P(class, prefix, bouncefactor, float, NONE) \
23 P(class, prefix, bouncestop, float, NONE) \
24 P(class, prefix, damageforcescale, float, BOTH) \
25 P(class, prefix, damage, float, BOTH) \
26 P(class, prefix, edgedamage, float, BOTH) \
27 P(class, prefix, force, float, BOTH) \
28 P(class, prefix, health, float, BOTH) \
29 P(class, prefix, lifetime, float, BOTH) \
30 P(class, prefix, lifetime_bounce, float, SEC) \
31 P(class, prefix, lifetime_stick, float, BOTH) \
32 P(class, prefix, radius, float, BOTH) \
33 P(class, prefix, refire, float, BOTH) \
34 P(class, prefix, reload_ammo, float, NONE) \
35 P(class, prefix, reload_time, float, NONE) \
36 P(class, prefix, remote_detonateprimary, float, SEC) \
37 P(class, prefix, remote_minbouncecnt, float, PRI) \
38 P(class, prefix, speed, float, BOTH) \
39 P(class, prefix, speed_up, float, BOTH) \
40 P(class, prefix, speed_z, float, BOTH) \
41 P(class, prefix, spread, float, BOTH) \
42 P(class, prefix, switchdelay_drop, float, NONE) \
43 P(class, prefix, switchdelay_raise, float, NONE) \
44 P(class, prefix, type, float, BOTH) \
45 P(class, prefix, weaponreplace, string, NONE) \
46 P(class, prefix, weaponstartoverride, float, NONE) \
47 P(class, prefix, weaponstart, float, NONE) \
48 P(class, prefix, weaponthrowable, float, NONE) \
50 W_PROPS(X, Mortar, mortar)
53 REGISTER_WEAPON(MORTAR, mortar, NEW(Mortar));
57 .float gl_detonate_later;
64 spawnfunc(weapon_mortar) { weapon_defaultspawnfunc(this, WEP_MORTAR); }
65 spawnfunc(weapon_grenadelauncher) { spawnfunc_weapon_mortar(this); }
67 void W_Mortar_Grenade_Explode()
69 if(other.takedamage == DAMAGE_AIM)
71 if(DIFF_TEAM(self.realowner, other))
74 Send_Notification(NOTIF_ONE, self.realowner, MSG_ANNCE, ANNCE_ACHIEVEMENT_AIRSHOT);
76 self.event_damage = func_null;
77 self.takedamage = DAMAGE_NO;
79 if(self.movetype == MOVETYPE_NONE)
80 self.velocity = self.oldvelocity;
82 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);
87 void W_Mortar_Grenade_Explode2()
89 if(other.takedamage == DAMAGE_AIM)
91 if(DIFF_TEAM(self.realowner, other))
94 Send_Notification(NOTIF_ONE, self.realowner, MSG_ANNCE, ANNCE_ACHIEVEMENT_AIRSHOT);
96 self.event_damage = func_null;
97 self.takedamage = DAMAGE_NO;
99 if(self.movetype == MOVETYPE_NONE)
100 self.velocity = self.oldvelocity;
102 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);
108 void W_Mortar_Grenade_Damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
113 if(!W_CheckProjectileDamage(inflictor.realowner, this.realowner, deathtype, -1)) // no exceptions
114 return; // g_projectiles_damage says to halt
116 this.health = this.health - damage;
119 WITH(entity, self, this, W_PrepareExplosionByDamage(attacker, this.use));
122 void W_Mortar_Grenade_Think1()
124 self.nextthink = time;
128 self.projectiledeathtype |= HITTYPE_BOUNCE;
129 W_Mortar_Grenade_Explode();
132 if(self.gl_detonate_later && self.gl_bouncecnt >= WEP_CVAR_PRI(mortar, remote_minbouncecnt))
133 W_Mortar_Grenade_Explode();
136 void W_Mortar_Grenade_Touch1()
139 if(other.takedamage == DAMAGE_AIM || WEP_CVAR_PRI(mortar, type) == 0) // always explode when hitting a player, or if normal mortar projectile
143 else if(WEP_CVAR_PRI(mortar, type) == 1) // bounce
145 spamsound(self, CH_SHOTS, SND(GRENADE_BOUNCE_RANDOM()), VOL_BASE, ATTN_NORM);
146 Send_Effect(EFFECT_HAGAR_BOUNCE, self.origin, self.velocity, 1);
147 self.projectiledeathtype |= HITTYPE_BOUNCE;
148 self.gl_bouncecnt += 1;
150 else if(WEP_CVAR_PRI(mortar, type) == 2 && (!other || (other.takedamage != DAMAGE_AIM && other.movetype == MOVETYPE_NONE))) // stick
152 spamsound(self, CH_SHOTS, SND(GRENADE_STICK), VOL_BASE, ATTN_NORM);
154 // let it stick whereever it is
155 self.oldvelocity = self.velocity;
156 self.velocity = '0 0 0';
157 self.movetype = MOVETYPE_NONE; // also disables gravity
158 self.gravity = 0; // nope, it does NOT! maybe a bug in CSQC code? TODO
159 UpdateCSQCProjectile(self);
161 // do not respond to any more touches
162 self.solid = SOLID_NOT;
164 self.nextthink = min(self.nextthink, time + WEP_CVAR_PRI(mortar, lifetime_stick));
168 void W_Mortar_Grenade_Touch2()
171 if(other.takedamage == DAMAGE_AIM || WEP_CVAR_SEC(mortar, type) == 0) // always explode when hitting a player, or if normal mortar projectile
175 else if(WEP_CVAR_SEC(mortar, type) == 1) // bounce
177 spamsound(self, CH_SHOTS, SND(GRENADE_BOUNCE_RANDOM()), VOL_BASE, ATTN_NORM);
178 Send_Effect(EFFECT_HAGAR_BOUNCE, self.origin, self.velocity, 1);
179 self.projectiledeathtype |= HITTYPE_BOUNCE;
180 self.gl_bouncecnt += 1;
182 if(WEP_CVAR_SEC(mortar, lifetime_bounce) && self.gl_bouncecnt == 1)
183 self.nextthink = time + WEP_CVAR_SEC(mortar, lifetime_bounce);
186 else if(WEP_CVAR_SEC(mortar, type) == 2 && (!other || (other.takedamage != DAMAGE_AIM && other.movetype == MOVETYPE_NONE))) // stick
188 spamsound(self, CH_SHOTS, SND(GRENADE_STICK), VOL_BASE, ATTN_NORM);
190 // let it stick whereever it is
191 self.oldvelocity = self.velocity;
192 self.velocity = '0 0 0';
193 self.movetype = MOVETYPE_NONE; // also disables gravity
194 self.gravity = 0; // nope, it does NOT! maybe a bug in CSQC code? TODO
195 UpdateCSQCProjectile(self);
197 // do not respond to any more touches
198 self.solid = SOLID_NOT;
200 self.nextthink = min(self.nextthink, time + WEP_CVAR_SEC(mortar, lifetime_stick));
204 void W_Mortar_Attack(Weapon thiswep)
208 W_DecreaseAmmo(thiswep, self, WEP_CVAR_PRI(mortar, ammo));
210 W_SetupShot_ProjectileSize(self, '-3 -3 -3', '3 3 3', false, 4, SND(GRENADE_FIRE), CH_WEAPON_A, WEP_CVAR_PRI(mortar, damage));
211 w_shotdir = v_forward; // no TrueAim for grenades please
213 Send_Effect(EFFECT_GRENADE_MUZZLEFLASH, w_shotorg, w_shotdir * 1000, 1);
216 gren.owner = gren.realowner = self;
217 gren.bot_dodge = true;
218 gren.bot_dodgerating = WEP_CVAR_PRI(mortar, damage);
219 gren.movetype = MOVETYPE_BOUNCE;
220 gren.bouncefactor = WEP_CVAR(mortar, bouncefactor);
221 gren.bouncestop = WEP_CVAR(mortar, bouncestop);
222 PROJECTILE_MAKETRIGGER(gren);
223 gren.projectiledeathtype = WEP_MORTAR.m_id;
224 setorigin(gren, w_shotorg);
225 setsize(gren, '-3 -3 -3', '3 3 3');
227 gren.cnt = time + WEP_CVAR_PRI(mortar, lifetime);
228 gren.nextthink = time;
229 gren.think = W_Mortar_Grenade_Think1;
230 gren.use = W_Mortar_Grenade_Explode;
231 gren.touch = W_Mortar_Grenade_Touch1;
233 gren.takedamage = DAMAGE_YES;
234 gren.health = WEP_CVAR_PRI(mortar, health);
235 gren.damageforcescale = WEP_CVAR_PRI(mortar, damageforcescale);
236 gren.event_damage = W_Mortar_Grenade_Damage;
237 gren.damagedbycontents = true;
238 gren.missile_flags = MIF_SPLASH | MIF_ARC;
239 W_SetupProjVelocity_UP_PRI(gren, mortar);
241 gren.angles = vectoangles(gren.velocity);
242 gren.flags = FL_PROJECTILE;
244 if(WEP_CVAR_PRI(mortar, type) == 0 || WEP_CVAR_PRI(mortar, type) == 2)
245 CSQCProjectile(gren, true, PROJECTILE_GRENADE, true);
247 CSQCProjectile(gren, true, PROJECTILE_GRENADE_BOUNCING, true);
249 MUTATOR_CALLHOOK(EditProjectile, self, gren);
252 void W_Mortar_Attack2(Weapon thiswep)
256 W_DecreaseAmmo(thiswep, self, WEP_CVAR_SEC(mortar, ammo));
258 W_SetupShot_ProjectileSize(self, '-3 -3 -3', '3 3 3', false, 4, SND(GRENADE_FIRE), CH_WEAPON_A, WEP_CVAR_SEC(mortar, damage));
259 w_shotdir = v_forward; // no TrueAim for grenades please
261 Send_Effect(EFFECT_GRENADE_MUZZLEFLASH, w_shotorg, w_shotdir * 1000, 1);
264 gren.owner = gren.realowner = self;
265 gren.bot_dodge = true;
266 gren.bot_dodgerating = WEP_CVAR_SEC(mortar, damage);
267 gren.movetype = MOVETYPE_BOUNCE;
268 gren.bouncefactor = WEP_CVAR(mortar, bouncefactor);
269 gren.bouncestop = WEP_CVAR(mortar, bouncestop);
270 PROJECTILE_MAKETRIGGER(gren);
271 gren.projectiledeathtype = WEP_MORTAR.m_id | HITTYPE_SECONDARY;
272 setorigin(gren, w_shotorg);
273 setsize(gren, '-3 -3 -3', '3 3 3');
275 gren.nextthink = time + WEP_CVAR_SEC(mortar, lifetime);
276 gren.think = adaptor_think2use_hittype_splash;
277 gren.use = W_Mortar_Grenade_Explode2;
278 gren.touch = W_Mortar_Grenade_Touch2;
280 gren.takedamage = DAMAGE_YES;
281 gren.health = WEP_CVAR_SEC(mortar, health);
282 gren.damageforcescale = WEP_CVAR_SEC(mortar, damageforcescale);
283 gren.event_damage = W_Mortar_Grenade_Damage;
284 gren.damagedbycontents = true;
285 gren.missile_flags = MIF_SPLASH | MIF_ARC;
286 W_SetupProjVelocity_UP_SEC(gren, mortar);
288 gren.angles = vectoangles(gren.velocity);
289 gren.flags = FL_PROJECTILE;
291 if(WEP_CVAR_SEC(mortar, type) == 0 || WEP_CVAR_SEC(mortar, type) == 2)
292 CSQCProjectile(gren, true, PROJECTILE_GRENADE, true);
294 CSQCProjectile(gren, true, PROJECTILE_GRENADE_BOUNCING, true);
296 MUTATOR_CALLHOOK(EditProjectile, self, gren);
299 .float bot_secondary_grenademooth;
301 METHOD(Mortar, wr_aim, void(entity thiswep))
303 self.BUTTON_ATCK = false;
304 self.BUTTON_ATCK2 = false;
305 if(self.bot_secondary_grenademooth == 0) // WEAPONTODO: merge this into using WEP_CVAR_BOTH
307 if(bot_aim(WEP_CVAR_PRI(mortar, speed), WEP_CVAR_PRI(mortar, speed_up), WEP_CVAR_PRI(mortar, lifetime), true))
309 self.BUTTON_ATCK = true;
310 if(random() < 0.01) self.bot_secondary_grenademooth = 1;
315 if(bot_aim(WEP_CVAR_SEC(mortar, speed), WEP_CVAR_SEC(mortar, speed_up), WEP_CVAR_SEC(mortar, lifetime), true))
317 self.BUTTON_ATCK2 = true;
318 if(random() < 0.02) self.bot_secondary_grenademooth = 0;
324 wepinfo_pri_refire = max3(sys_frametime, WEP_CVAR_PRI(mortar, refire), WEP_CVAR_PRI(mortar, animtime));
325 wepinfo_pri_dps = (WEP_CVAR_PRI(mortar, damage) * (1 / wepinfo_pri_refire));
326 wepinfo_pri_speed = (1 / max(1, (10000 / max(1, WEP_CVAR_PRI(mortar, speed)))));
328 // for the range calculation, closer to 1 is better
329 wepinfo_pri_range_max = 2000 * wepinfo_pri_speed;
330 wepinfo_pri_range = wepinfo_pri_speed * WEP_CVAR_PRI(mortar,
332 wepinfo_sec_refire = max3(sys_frametime, WEP_CVAR_SEC(mortar, refire), WEP_CVAR_SEC(mortar, animtime));
333 wepinfo_sec_dps = (WEP_CVAR_SEC(mortar, damage) * (1 / wepinfo_sec_refire));
335 wepinfo_sec_dps = (WEP_CVAR_SEC(mortar, damage) * (1 / max3(sys_frametime, WEP_CVAR_SEC(mortar, refire), WEP_CVAR_SEC(mortar, animtime))));
338 METHOD(Mortar, wr_think, void(entity thiswep, entity actor, .entity weaponentity, int fire))
340 if(autocvar_g_balance_mortar_reload_ammo && actor.clip_load < min(WEP_CVAR_PRI(mortar, ammo), WEP_CVAR_SEC(mortar, ammo))) { // forced reload
341 thiswep.wr_reload(thiswep, actor, weaponentity);
344 if(weapon_prepareattack(thiswep, actor, weaponentity, false, WEP_CVAR_PRI(mortar, refire)))
346 W_Mortar_Attack(thiswep);
347 weapon_thinkf(actor, weaponentity, WFRAME_FIRE1, WEP_CVAR_PRI(mortar, animtime), w_ready);
352 if(WEP_CVAR_SEC(mortar, remote_detonateprimary))
354 bool nadefound = false;
356 for(nade = world; (nade = find(nade, classname, "grenade")); ) if(nade.realowner == actor)
358 if(!nade.gl_detonate_later)
360 nade.gl_detonate_later = true;
365 sound(actor, CH_WEAPON_B, SND_ROCKET_DET, VOL_BASE, ATTN_NORM);
367 else if(weapon_prepareattack(thiswep, actor, weaponentity, true, WEP_CVAR_SEC(mortar, refire)))
369 W_Mortar_Attack2(thiswep);
370 weapon_thinkf(actor, weaponentity, WFRAME_FIRE2, WEP_CVAR_SEC(mortar, animtime), w_ready);
374 METHOD(Mortar, wr_checkammo1, bool(entity thiswep))
376 float ammo_amount = self.(thiswep.ammo_field) >= WEP_CVAR_PRI(mortar, ammo);
377 ammo_amount += self.(weapon_load[WEP_MORTAR.m_id]) >= WEP_CVAR_PRI(mortar, ammo);
380 METHOD(Mortar, wr_checkammo2, bool(entity thiswep))
382 float ammo_amount = self.(thiswep.ammo_field) >= WEP_CVAR_SEC(mortar, ammo);
383 ammo_amount += self.(weapon_load[WEP_MORTAR.m_id]) >= WEP_CVAR_SEC(mortar, ammo);
386 METHOD(Mortar, wr_reload, void(entity thiswep, entity actor, .entity weaponentity))
388 W_Reload(self, min(WEP_CVAR_PRI(mortar, ammo), WEP_CVAR_SEC(mortar, ammo)), SND(RELOAD)); // WEAPONTODO
390 METHOD(Mortar, wr_suicidemessage, int(entity thiswep))
392 if(w_deathtype & HITTYPE_SECONDARY)
393 return WEAPON_MORTAR_SUICIDE_BOUNCE;
395 return WEAPON_MORTAR_SUICIDE_EXPLODE;
397 METHOD(Mortar, wr_killmessage, int(entity thiswep))
399 if(w_deathtype & HITTYPE_SECONDARY)
400 return WEAPON_MORTAR_MURDER_BOUNCE;
402 return WEAPON_MORTAR_MURDER_EXPLODE;
408 METHOD(Mortar, wr_impacteffect, void(entity thiswep))
411 org2 = w_org + w_backoff * 12;
412 pointparticles(EFFECT_GRENADE_EXPLODE, org2, '0 0 0', 1);
414 sound(self, CH_SHOTS, SND_GRENADE_IMPACT, VOL_BASE, ATTN_NORM);