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