]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/nexball.qc
Nexball goes official :D
[divverent/nexuiz.git] / data / qcsrc / server / nexball.qc
1 //EF_BRIGHTFIELD|EF_BRIGHTLIGHT|EF_DIMLIGHT|EF_BLUE|EF_RED|EF_FLAME
2 #define BALL_EFFECTMASK 1229
3 #define BALL_MINS '-16 -16 -16'  // The model is 24*24*24
4 #define BALL_MAXS '16 16 16'
5 #define BALL_ATTACHORG '3 0 16'
6 #define BALL_FOOT   1
7 #define BALL_BASKET 2
8 #define GOAL_TOUCHPLAYER 1
9 #define CVTOV(s) s = cvar( #s )
10
11 float g_nexball_football_boost_forward;
12 float g_nexball_football_boost_up;
13 float g_nexball_football_physics;
14 float g_nexball_delay_idle;
15 float g_nexball_basketball_delay_hold;
16 float g_nexball_basketball_delay_hold_forteam;
17 float g_nexball_basketball_effects_default;
18 float g_nexball_basketball_teamsteal;
19 float balls;
20 float ball_scale;
21
22 .float teamtime;
23
24 void nb_init() // Called early (worldspawn stage)
25 {
26         CVTOV(g_nexball_meter_period); //sent with the client init entity
27         if (g_nexball_meter_period <= 0)
28                 g_nexball_meter_period = 2; // avoid division by zero etc. due to silly users
29         g_nexball_meter_period = rint(g_nexball_meter_period * 32) / 32; //Round to 1/32ths to send as a byte multiplied by 32
30         addstat(STAT_NB_METERSTART, AS_FLOAT, metertime);
31
32         // General settings
33         CVTOV(g_nexball_football_boost_forward);   //100
34         CVTOV(g_nexball_football_boost_up);        //200
35         CVTOV(g_nexball_delay_idle);               //10
36         CVTOV(g_nexball_football_physics);         //0
37 }
38
39 void ResetBall();
40
41 void LogNB(string mode, entity actor)
42 {
43         string s;
44         if(!cvar("sv_eventlog"))
45                 return;
46         s = strcat(":nexball:", mode);
47         if(actor != world)
48                 s = strcat(s, ":", ftos(actor.playerid));
49         GameLogEcho(s);
50 }
51
52 void ball_restart (void)
53 {
54         if(self.owner)
55                 DropBall(self, self.owner.origin, '0 0 0');
56         ResetBall();
57 }
58
59 void nexball_setstatus (void)
60 {
61         local entity oldself;
62         if (!g_nexball)
63                 return;
64         if (self.ballcarried)
65         {
66                 if (self.ballcarried.teamtime && (self.ballcarried.teamtime < time))
67                 {
68                         bprint("The ", ColoredTeamName(self.team), " held the ball for too long.\n");
69                         oldself = self;
70                         self = self.ballcarried;
71                         DropBall(self, self.owner.origin, '0 0 0');
72                         ResetBall();
73                         self = oldself;
74                 } else
75                         self.items |= IT_KEY1;
76         }
77 }
78
79 void relocate_nexball (void)
80 {
81         tracebox(self.origin, BALL_MINS, BALL_MAXS, self.origin, TRUE, self);
82         if (trace_startsolid)
83         {
84                 vector o;
85                 o = self.origin;
86                 if(!move_out_of_solid(self))
87                         objerror("could not get out of solid at all!");
88                 print("^1NOTE: this map needs FIXING. ", self.classname, " at ", vtos(o - '0 0 1'));
89                 print(" needs to be moved out of solid, e.g. by '", ftos(self.origin_x - o_x));
90                 print(" ", ftos(self.origin_y - o_y));
91                 print(" ", ftos(self.origin_z - o_z), "'\n");
92                 self.origin = o;
93         }
94 }
95
96 void basketball_touch();
97 void football_touch();
98
99 void DropOwner (void)
100 {
101         local entity ownr;
102         ownr = self.owner;
103         DropBall(self, ownr.origin, ownr.velocity);
104         makevectors(ownr.v_angle_y * '0 1 0');
105         ownr.velocity += ('0 0 0.75' - v_forward) * 1000;
106         ownr.flags &~= FL_ONGROUND;
107 }
108
109 void DropBall (entity ball, vector org, vector vel)
110 {
111         ball.effects |= g_nexball_basketball_effects_default;
112         ball.effects &~= EF_NOSHADOW;
113         ball.owner.effects &~= g_nexball_basketball_effects_default; // this may be problematic.
114
115         setattachment(ball, world, "");
116         setorigin (ball, org);
117         ball.movetype = MOVETYPE_BOUNCE;
118         ball.flags &~= FL_ONGROUND;
119         ball.scale = ball_scale;
120         ball.velocity = vel;
121         ball.ctf_droptime = time;
122         ball.touch = basketball_touch;
123         ball.think = ResetBall;
124         ball.nextthink = min(time + g_nexball_delay_idle, ball.teamtime);
125
126         if (ball.owner.metertime)
127         {
128                 ball.owner.metertime = 0;
129                 ball.owner.weaponentity.state = WS_READY;
130         }
131
132         ball.owner.ballcarried = world;
133         ball.owner = world;
134 }
135
136 void InitBall (void)
137 {
138         if (gameover) return;
139         self.flags &~= FL_ONGROUND;
140         self.movetype = MOVETYPE_BOUNCE;
141         if (self.classname == "ball_basketball")
142                 self.touch = basketball_touch;
143         else if (self.classname == "ball_football")
144                 self.touch = football_touch;
145         self.cnt = 0;
146         self.think = ResetBall;
147         self.nextthink = time + g_nexball_delay_idle + 3;
148         self.teamtime = 0;
149         self.pusher = world;
150         self.team = FALSE;
151         sound (self, CHAN_PROJECTILE, self.noise1, VOL_BASE, ATTN_NORM);
152         LogNB("init", world);
153 }
154
155 void ResetBall (void)
156 {
157         if (self.cnt < 2) { // step 1
158                 if (time == self.teamtime)
159                         bprint("The ", ColoredTeamName(self.team), " held the ball for too long.\n");
160                 self.touch = SUB_Null;
161                 self.movetype = MOVETYPE_NOCLIP;
162                 self.velocity = '0 0 0'; // just in case?
163                 if(!self.cnt)
164                         LogNB("resetidle", world);
165                 self.cnt = 2;
166                 self.nextthink = time;
167         } else if (self.cnt < 4) { // step 2 and 3
168 //              dprint("Step ", ftos(self.cnt), ": Calculated velocity: ", vtos(self.spawnorigin - self.origin), ", time: ", ftos(time), "\n");
169                 self.velocity = (self.spawnorigin - self.origin) * (self.cnt - 1); // 1 or 0.5 second movement
170                 self.nextthink = time + 0.5;
171                 self.cnt += 1;
172         } else { // step 4
173 //              dprint("Step 4: time: ", ftos(time), "\n");
174                 if (vlen(self.origin - self.spawnorigin) > 10) // should not happen anymore
175                         dprint("The ball moved too far away from its spawn origin.\nOffset: ",
176                                vtos(self.origin - self.spawnorigin), " Velocity: ", vtos(self.velocity), "\n");
177                 self.velocity = '0 0 0';
178                 setorigin(self, self.spawnorigin); // make sure it's positioned correctly anyway
179                 self.movetype = MOVETYPE_NONE;
180                 self.think = InitBall;
181                 self.nextthink = max(time, game_starttime) + cvar("g_nexball_delay_start");
182         }
183 }
184
185 void football_touch (void)
186 {
187         if (other.solid == SOLID_BSP) {
188                 if (time > self.lastground + 0.1)
189                 {
190                         sound (self, CHAN_PROJECTILE, self.noise, VOL_BASE, ATTN_NORM);
191                         self.lastground = time;
192                 }
193                 if (vlen(self.velocity) && !self.cnt)
194                         self.nextthink = time + g_nexball_delay_idle;
195                 return;
196         }
197         if (other.classname != "player")
198                 return;
199         if (other.health < 1)
200                 return;
201         if (!self.cnt)
202                 self.nextthink = time + g_nexball_delay_idle;
203
204         self.pusher = other;
205         self.team = other.team;
206
207         if (g_nexball_football_physics == -1) { // MrBougo try 1, before decompiling Rev's original
208                 if (vlen(other.velocity))
209                         self.velocity = other.velocity * 1.5 + '0 0 1' * g_nexball_football_boost_up;
210         } else if (g_nexball_football_physics == 1) { // MrBougo's modded Rev style: partially independant of the height of the aiming point
211                 makevectors(other.v_angle);
212                 self.velocity = other.velocity + v_forward * g_nexball_football_boost_forward + '0 0 1' * g_nexball_football_boost_up;
213         } else if (g_nexball_football_physics == 2) { // 2nd mod try: totally independant. Really playable!
214                 makevectors(other.v_angle_y * '0 1 0');
215                 self.velocity = other.velocity + v_forward * g_nexball_football_boost_forward + v_up * g_nexball_football_boost_up;
216         } else { // Revenant's original style (from the original mod's disassembly, acknowledged by Revenant)
217                 makevectors(other.v_angle);
218                 self.velocity = other.velocity + v_forward * g_nexball_football_boost_forward + v_up * g_nexball_football_boost_up;
219         }
220         self.avelocity = -250 * v_forward;  // maybe there is a way to make it look better?
221 }
222
223 void basketball_touch (void)
224 {
225         if (other.ballcarried)
226         {
227                 football_touch();
228                 return;
229         }
230         if (!self.cnt && other.classname == "player" && (other.playerid != self.dropperid || time > self.ctf_droptime + cvar("g_nexball_delay_collect"))) {
231                 if (other.health <= 0)
232                         return;
233                 LogNB("caught", other);
234
235                 if (self.team != other.team)
236                         self.teamtime = time + g_nexball_basketball_delay_hold_forteam;
237
238                 self.owner = self.pusher = other;
239                 self.team = other.team;
240                 other.ballcarried = self;
241                 other.effects = other.effects | g_nexball_basketball_effects_default;
242                 self.effects &~= g_nexball_basketball_effects_default;
243                 self.effects |= EF_NOSHADOW;
244                 self.scale = 1; // scale down.
245                 self.dropperid = other.playerid;
246                 setattachment(self, other, "");
247                 setorigin(self, BALL_ATTACHORG);
248                 self.velocity = '0 0 0';
249                 self.movetype = MOVETYPE_NONE;
250                 self.touch = SUB_Null;
251                 if (g_nexball_basketball_delay_hold)
252                 {
253                         self.think = DropOwner;
254                         self.nextthink = time + g_nexball_basketball_delay_hold;
255                 }
256         } else if (other.solid == SOLID_BSP) {
257                 sound (self, CHAN_PROJECTILE, self.noise, VOL_BASE, ATTN_NORM);
258                 if (vlen(self.velocity) && !self.cnt)
259                         self.nextthink = min(time + g_nexball_delay_idle, self.teamtime);
260         }
261 }
262
263 void ReturnTouch (void)
264 {
265         string othername;
266         entity tempother;
267         tempother = other;
268
269         if (gameover) return;
270         if ((self.spawnflags & GOAL_TOUCHPLAYER) && tempother.ballcarried)
271                 tempother = tempother.ballcarried; // Can't drop here yet because of EXACTTRIGGER
272         if (tempother.classname != "ball_basketball")
273         if (tempother.classname != "ball_football")
274                 return;
275         if (tempother.cnt)
276                 return;
277         EXACTTRIGGER_TOUCH;
278         other = tempother;
279
280         if (other.owner)
281         {
282                 othername = other.owner.netname;
283                 DropBall(other, other.owner.origin, '0 0 0');
284         }
285         else
286                 othername = "The ball";
287
288         LogNB("out", other.pusher);
289         if (self.spawnflags & GOAL_TOUCHPLAYER)
290                 bprint(othername, "^7 went out of bounds.\n");
291         else
292                 bprint("The ball was returned.\n");
293         sound (other, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NONE);
294
295         other.cnt = 1;
296         other.think = ResetBall;
297         if (other.classname == "ball_basketball")
298                 other.touch = football_touch;
299         other.nextthink = time + 1.5;
300 }
301
302 void GoalTouch (void)
303 {
304         string othername;
305         entity tempother;
306         float isclient;
307         tempother = other; // EXACTTRIGGER uses "other"
308
309         if (gameover) return;
310         if ((self.spawnflags & GOAL_TOUCHPLAYER) && tempother.ballcarried)
311                 tempother = tempother.ballcarried; // Can't drop here yet because of EXACTTRIGGER
312         if (tempother.classname != "ball_basketball")
313         if (tempother.classname != "ball_football")
314                 return;
315         if (!tempother.pusher || tempother.cnt)
316                 return;
317         EXACTTRIGGER_TOUCH;
318         other = tempother;
319
320         isclient = other.pusher.flags & FL_CLIENT;
321
322         if (isclient)
323                 othername = other.pusher.netname;
324         else
325                 othername = "Someone (?)";
326
327         if (self.classname == "ball_fault") { // FIXME?: this is ugly
328                      if (other.team == COLOR_TEAM1) self.team = COLOR_TEAM2;
329                 else if (other.team == COLOR_TEAM2) self.team = COLOR_TEAM1;
330                 else                                self.team = FALSE;
331
332                 if (isclient)
333                         PlayerScore_Add(other.pusher, SP_NEXBALL_OWNGOALS, 1);
334                 if (self.team)
335                 {
336                         bprint(ColoredTeamName(self.team), " gets awarded 1 point for ", othername, "^7's silliness.\n");
337                         LogNB("fault-score", other.pusher);
338                 }
339                 else
340                 {
341                         bprint("The ball was reset.\n");
342                         LogNB("fault-reset", other.pusher);
343                 }
344         } else if (other.team == self.team) { // the goal's .team is set to the team it gives points to
345                 if (isclient)
346                         PlayerScore_Add(other.pusher, SP_NEXBALL_GOALS, 1);
347                 bprint("Goaaaaal! ", othername, "^7 scored a point for the ", ColoredTeamName(self.team), ".\n");
348                 LogNB("goal", other.pusher);
349         } else if (other.team) {
350                 if (isclient)
351                         PlayerScore_Add(other.pusher, SP_NEXBALL_OWNGOALS, 1);
352                 bprint("Boo! ", othername, "^7 scored a point for the opposing ", ColoredTeamName(self.team), ".\n");
353                 LogNB("owngoal", other.pusher);
354         } else {
355                 bprint(othername, "^7 scored a point for the ", ColoredTeamName(self.team), ".\n"); //Should not happen
356                 LogNB("goal-noteam", other.pusher);
357         }
358         sound (other, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NONE);
359
360         if (self.team)
361                 TeamScore_AddToTeam(self.team, ST_NEXBALL_GOALS, 1);
362
363         if (other.owner) // Happens on spawnflag GOAL_TOUCHPLAYER
364                 DropBall(other, other.owner.origin, other.owner.velocity);
365         other.cnt = 1;
366         other.think = ResetBall;
367         if (other.classname == "ball_basketball")
368                 other.touch = football_touch; // better than SUB_Null: football control until the ball gets reset
369         other.nextthink = time + cvar("g_nexball_delay_goal");
370 }
371
372 //=======================//
373 //      spawnfuncs       //
374 //=======================//
375
376 void SpawnBall (void)
377 {
378         if(!g_nexball) { remove(self); return; }
379
380         if (!self.model) {
381                 self.model = "models/nexball/ball.md3";
382                 self.scale = 1.3;
383         }
384
385         precache_model (self.model);
386         setmodel (self, self.model);
387         setsize (self, BALL_MINS, BALL_MAXS);
388         ball_scale = self.scale;
389
390         relocate_nexball();
391         self.spawnorigin = self.origin;
392
393         self.effects = self.effects | EF_LOWPRECISION;
394
395         if (cvar(strcat("g_nex", self.classname, "_trail")))
396         {
397                 self.glow_color = cvar("g_nexball_trail_color");
398                 self.glow_trail = TRUE;
399         }
400
401         self.movetype = MOVETYPE_FLY;
402
403         if (!cvar("g_nexball_sound_bounce"))
404                 self.noise = "";
405         else if (!self.noise)
406                 self.noise = "sound/nexball/bounce.ogg";
407                 //bounce sound placeholder (FIXME)
408         if (!self.noise1)
409                 self.noise1 = "sound/nexball/drop.ogg";
410                 //ball drop sound placeholder (FIXME)
411         if (!self.noise2)
412                 self.noise2 = "sound/nexball/steal.ogg";
413                 //stealing sound placeholder (FIXME)
414         if (self.noise) precache_sound (self.noise);
415         precache_sound (self.noise1);
416         precache_sound (self.noise2);
417
418         self.reset = ball_restart;
419         self.think = InitBall;
420         self.nextthink = game_starttime + cvar("g_nexball_delay_start");
421 }
422
423 void spawnfunc_ball_basketball (void)
424 {
425         if(!balls & BALL_BASKET)
426         {
427                 CVTOV(g_nexball_basketball_effects_default);
428                 CVTOV(g_nexball_basketball_delay_hold);
429                 CVTOV(g_nexball_basketball_delay_hold_forteam);
430                 CVTOV(g_nexball_basketball_teamsteal);
431                 g_nexball_basketball_effects_default = g_nexball_basketball_effects_default & BALL_EFFECTMASK;
432         }
433         if (!self.effects)
434                 self.effects = g_nexball_basketball_effects_default;
435         self.solid = SOLID_TRIGGER;
436
437         balls |= BALL_BASKET;
438
439         SpawnBall();
440 }
441
442 void spawnfunc_ball_football (void)
443 {
444         self.solid = SOLID_TRIGGER;
445
446         balls |= BALL_FOOT;
447
448         SpawnBall();
449 }
450
451 void spawnfunc_ball (void)
452 {
453         self.classname = "ball_football";
454         spawnfunc_ball_football();
455 }
456
457 // The "red goal" is defended by blue team. A ball in there counts as a point for red.
458 void spawnfunc_ball_redgoal (void)
459 {
460         if(!g_nexball) { remove(self); return; }
461         //COLOR_TEAM1 is red
462         EXACTTRIGGER_INIT;
463         self.team = COLOR_TEAM1;
464         if (!self.noise)
465                 self.noise = "ctf/redcapture.wav";
466         precache_sound(self.noise);
467         self.touch = GoalTouch;
468 }
469
470 //Same comment.
471 void spawnfunc_ball_bluegoal (void)
472 {
473         if(!g_nexball) { remove(self); return; }
474         EXACTTRIGGER_INIT;
475         self.team = COLOR_TEAM2;
476         if (!self.noise)
477                 self.noise = "ctf/bluecapture.wav";
478         precache_sound(self.noise);
479         self.touch = GoalTouch;
480 }
481
482 void spawnfunc_ball_fault (void)
483 {
484         if(!g_nexball) { remove(self); return; }
485         EXACTTRIGGER_INIT;
486         if (!self.noise)
487                 self.noise = "misc/typehit.wav";
488         precache_sound(self.noise);
489         self.touch = GoalTouch;
490 }
491
492 void spawnfunc_ball_bound (void)
493 {
494         if(!g_nexball) { remove(self); return; }
495         EXACTTRIGGER_INIT;
496         if (!self.noise)
497                 self.noise = "misc/typehit.wav";
498         precache_sound(self.noise);
499         self.touch = ReturnTouch;
500 }
501
502
503 //=======================//
504 //      Weapon code      //
505 //=======================//
506
507 void W_Nexball_Touch (void)
508 {
509         local entity ball, attacker;
510         attacker = self.owner;
511
512         PROJECTILE_TOUCH;
513         if(attacker.team != other.team || g_nexball_basketball_teamsteal)
514         if((ball = other.ballcarried))
515         {
516                 other.velocity = other.velocity + normalize(self.velocity) * other.damageforcescale * cvar("g_balance_nexball_secondary_force");
517                 other.flags &~= FL_ONGROUND;
518                 if(!attacker.ballcarried)
519                 {
520                         other.effects &~= g_nexball_basketball_effects_default;
521                         other.ballcarried = world;
522
523                         if (g_nexball_basketball_delay_hold)
524                         {
525                                 ball.think = DropOwner;
526                                 ball.nextthink = time + g_nexball_basketball_delay_hold;
527                         }
528
529                         attacker.effects = attacker.effects | g_nexball_basketball_effects_default;
530                         attacker.ballcarried = ball;
531                         if (other.metertime)
532                         {
533                                 other.metertime = 0;
534                                 other.weaponentity.state = WS_READY;
535                         }
536
537                         setattachment(ball, attacker, "");
538                         ball.owner = ball.pusher = attacker;
539                         ball.team = attacker.team;
540                         ball.dropperid = attacker.playerid;
541
542                         if (other.team != attacker.team)
543                                 ball.teamtime = time + g_nexball_basketball_delay_hold_forteam;
544
545                         LogNB("stole", attacker);
546                         sound (other, CHAN_AUTO, ball.noise2, VOL_BASE, ATTN_NORM);
547                 }
548         }
549         remove(self);
550 }
551
552 void W_Nexball_Attack (float t)
553 {
554         local entity ball;
555         local float mul, mi, ma;
556         if (!(ball = self.ballcarried))
557                 return;
558
559         W_SetupShot (self, FALSE, 4, "weapons/grenade_fire.wav");
560         tracebox(w_shotorg, BALL_MINS, BALL_MAXS, w_shotorg, MOVE_WORLDONLY, world);
561         if(trace_startsolid)
562         {
563                 if(self.metertime)
564                         self.metertime = 0; // Shot failed, hide the power meter
565                 return;
566         }
567
568         //Calculate multiplier
569         if (t < 0)
570                 mul = 1;
571         else
572         {
573                 mi = cvar("g_nexball_basketball_meter_minpower");
574                 ma = max(mi, cvar("g_nexball_basketball_meter_maxpower")); // avoid confusion
575                 //One triangle wave period with 1 as max
576                 mul = 2 * mod(t, g_nexball_meter_period) / g_nexball_meter_period;
577                 if (mul > 1)
578                         mul = 2 - mul;
579                 mul = mi + (ma - mi) * mul; // range from the minimal power to the maximal power
580         }
581         DropBall (ball, w_shotorg, W_CalculateProjectileVelocity(self.velocity, w_shotdir * cvar("g_balance_nexball_primary_speed") * mul));
582         //TODO: use the speed_up cvar too ??
583 }
584
585 void W_Nexball_Attack2 (void)
586 {
587         local entity missile;
588         if (!(balls & BALL_BASKET))
589                 return;
590         W_SetupShot (self, FALSE, 2, "weapons/grenade_fire.wav");
591         pointparticles(particleeffectnum("grenadelauncher_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
592         missile = spawn ();
593
594         missile.owner = self;
595         missile.classname = "ballstealer";
596
597         missile.movetype = MOVETYPE_FLY;
598         missile.solid = SOLID_BBOX;
599
600         setmodel (missile, "models/elaser.mdl"); // precision set below
601         setsize (missile, '0 0 0', '0 0 0');
602         setorigin (missile, w_shotorg);
603
604         missile.velocity = w_shotdir * cvar("g_balance_nexball_secondary_speed");
605         W_SetupProjectileVelocity(missile);
606         missile.angles = vectoangles (missile.velocity);
607         missile.touch = W_Nexball_Touch;
608         missile.think = SUB_Remove;
609         missile.nextthink = time + cvar("g_balance_nexball_secondary_lifetime"); //FIXME: use a distance instead?
610
611         missile.effects = EF_BRIGHTFIELD | EF_LOWPRECISION;
612         missile.flags = FL_PROJECTILE;
613 }
614
615 float w_nexball_weapon(float req)
616 {
617         if (req == WR_THINK)
618         {
619                 if (self.BUTTON_ATCK)
620                 if (weapon_prepareattack(0, cvar("g_balance_nexball_primary_refire")))
621                 if (cvar("g_nexball_basketball_meter"))
622                 {
623                         if (self.ballcarried && !self.metertime)
624                                 self.metertime = time;
625                         else
626                                 weapon_thinkf(WFRAME_FIRE1, cvar("g_balance_nexball_primary_animtime"), w_ready);
627                 }
628                 else
629                 {
630                         W_Nexball_Attack(-1);
631                         weapon_thinkf(WFRAME_FIRE1, cvar("g_balance_nexball_primary_animtime"), w_ready);
632                 }
633                 if (self.BUTTON_ATCK2)
634                 if (weapon_prepareattack(1, cvar("g_balance_nexball_secondary_refire")))
635                 {
636                         W_Nexball_Attack2();
637                         weapon_thinkf(WFRAME_FIRE2, cvar("g_balance_nexball_secondary_animtime"), w_ready);
638                 }
639                 
640                 if (!self.BUTTON_ATCK && self.metertime && self.ballcarried)
641                 {
642                         W_Nexball_Attack(time - self.metertime);
643                         // DropBall or stealing will set metertime back to 0
644                         weapon_thinkf(WFRAME_FIRE1, cvar("g_balance_nexball_primary_animtime"), w_ready);
645                 }
646         }
647         else if (req == WR_PRECACHE)
648         {
649                 precache_model ("models/weapons/g_gl.md3");
650                 precache_model ("models/weapons/v_gl.md3");
651                 precache_model ("models/weapons/h_gl.dpm");
652                 precache_model ("models/elaser.mdl");
653                 precache_sound ("weapons/grenade_fire.wav");
654         }
655         else if (req == WR_SETUP)
656                 weapon_setup(WEP_GRENADE_LAUNCHER);
657         else if (req == WR_SUICIDEMESSAGE)
658         {
659                 w_deathtypestring = "is a weirdo";
660         }
661         else if (req == WR_KILLMESSAGE)
662         {
663                 w_deathtypestring = "got killed by #'s black magic";
664         }
665         // No need to check WR_CHECKAMMO* or WR_AIM, it should always return TRUE
666         return TRUE;
667 }