]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_nexball.qc
62cfd01ad867250673f406bfc68ab7ea99841671
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_nexball.qc
1 float autocvar_g_nexball_safepass_turnrate;
2 float autocvar_g_nexball_safepass_maxdist;
3 float autocvar_g_nexball_safepass_holdtime;
4 float autocvar_g_nexball_viewmodel_scale;
5 float autocvar_g_nexball_tackling;
6 vector autocvar_g_nexball_viewmodel_offset;
7
8 void basketball_touch();
9 void football_touch();
10 void ResetBall();
11 #define NBM_NONE 0
12 #define NBM_FOOTBALL 2
13 #define NBM_BASKETBALL 4
14 float nexball_mode;
15
16 float OtherTeam(float t)  //works only if there are two teams on the map!
17 {
18         entity e;
19         e = find(world, classname, "nexball_team");
20         if(e.team == t)
21                 e = find(e, classname, "nexball_team");
22         return e.team;
23 }
24
25
26 void LogNB(string mode, entity actor)
27 {
28         string s;
29         if(!autocvar_sv_eventlog)
30                 return;
31         s = strcat(":nexball:", mode);
32         if(actor != world)
33                 s = strcat(s, ":", ftos(actor.playerid));
34         GameLogEcho(s);
35 }
36
37 void ball_restart(void)
38 {
39         if(self.owner)
40                 DropBall(self, self.owner.origin, '0 0 0');
41         ResetBall();
42 }
43
44 void nexball_setstatus(void)
45 {
46         entity oldself;
47         self.items &~= IT_KEY1;
48         if(self.ballcarried)
49         {
50                 if(self.ballcarried.teamtime && (self.ballcarried.teamtime < time))
51                 {
52                         bprint("The ", ColoredTeamName(self.team), " held the ball for too long.\n");
53                         oldself = self;
54                         self = self.ballcarried;
55                         DropBall(self, self.owner.origin, '0 0 0');
56                         ResetBall();
57                         self = oldself;
58                 }
59                 else
60                         self.items |= IT_KEY1;
61         }
62 }
63
64 void relocate_nexball(void)
65 {
66         tracebox(self.origin, BALL_MINS, BALL_MAXS, self.origin, TRUE, self);
67         if(trace_startsolid)
68         {
69                 vector o;
70                 o = self.origin;
71                 if(!move_out_of_solid(self))
72                         objerror("could not get out of solid at all!");
73                 print("^1NOTE: this map needs FIXING. ", self.classname, " at ", vtos(o - '0 0 1'));
74                 print(" needs to be moved out of solid, e.g. by '", ftos(self.origin_x - o_x));
75                 print(" ", ftos(self.origin_y - o_y));
76                 print(" ", ftos(self.origin_z - o_z), "'\n");
77                 self.origin = o;
78         }
79 }
80
81 void DropOwner(void)
82 {
83         entity ownr;
84         ownr = self.owner;
85         DropBall(self, ownr.origin, ownr.velocity);
86         makevectors(ownr.v_angle_y * '0 1 0');
87         ownr.velocity += ('0 0 0.75' - v_forward) * 1000;
88         ownr.flags &~= FL_ONGROUND;
89 }
90
91 void GiveBall(entity plyr, entity ball)
92 {
93         entity ownr;
94
95         if((ownr = ball.owner))
96         {
97                 ownr.effects &~= autocvar_g_nexball_basketball_effects_default;
98                 ownr.ballcarried = world;
99                 if(ownr.metertime)
100                 {
101                         ownr.metertime = 0;
102                         ownr.weaponentity.state = WS_READY;
103                 }
104                 WaypointSprite_Kill(ownr.waypointsprite_attachedforcarrier);
105         }
106         else
107         {
108                 WaypointSprite_Kill(ball.waypointsprite_attachedforcarrier);
109         }
110         
111         //setattachment(ball, plyr, "");
112         setorigin(ball, plyr.origin + plyr.view_ofs);
113
114         if(ball.team != plyr.team)
115                 ball.teamtime = time + autocvar_g_nexball_basketball_delay_hold_forteam;
116
117         ball.owner = ball.pusher = plyr; //"owner" is set to the player carrying, "pusher" to the last player who touched it
118         ball.team = plyr.team;
119         plyr.ballcarried = ball;
120         ball.nb_dropper = plyr;
121
122         plyr.effects |= autocvar_g_nexball_basketball_effects_default;
123         ball.effects &~= autocvar_g_nexball_basketball_effects_default;
124
125         ball.velocity = '0 0 0';
126         ball.movetype = MOVETYPE_NONE;
127         ball.touch = func_null;
128         ball.effects |= EF_NOSHADOW;
129         ball.scale = 1; // scale down.
130
131         WaypointSprite_AttachCarrier("nb-ball", plyr, RADARICON_FLAGCARRIER, BALL_SPRITECOLOR);
132         WaypointSprite_UpdateRule(plyr.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
133
134         if(autocvar_g_nexball_basketball_delay_hold)
135         {
136                 ball.think = DropOwner;
137                 ball.nextthink = time + autocvar_g_nexball_basketball_delay_hold;
138         }
139         
140         ownr = self;
141         self = plyr;    
142         WEPSET_COPY_EE(self.weaponentity, self);
143         self.weaponentity.switchweapon = self.weapon;
144         WEPSET_COPY_EW(self, WEP_PORTO);
145         weapon_action(WEP_PORTO, WR_RESETPLAYER);
146         self.switchweapon = WEP_PORTO;
147         W_SwitchWeapon(WEP_PORTO);
148         self = ownr;
149 }
150
151 void DropBall(entity ball, vector org, vector vel)
152 {
153         ball.effects |= autocvar_g_nexball_basketball_effects_default;
154         ball.effects &~= EF_NOSHADOW;
155         ball.owner.effects &~= autocvar_g_nexball_basketball_effects_default;
156
157         setattachment(ball, world, "");
158         setorigin(ball, org);
159         ball.movetype = MOVETYPE_BOUNCE;
160         ball.flags &~= FL_ONGROUND;
161         ball.scale = ball_scale;
162         ball.velocity = vel;
163         ball.nb_droptime = time;
164         ball.touch = basketball_touch;
165         ball.think = ResetBall;
166         ball.nextthink = min(time + autocvar_g_nexball_delay_idle, ball.teamtime);
167
168         if(ball.owner.metertime)
169         {
170                 ball.owner.metertime = 0;
171                 ball.owner.weaponentity.state = WS_READY;
172         }
173
174         WaypointSprite_Kill(ball.owner.waypointsprite_attachedforcarrier);
175         WaypointSprite_Spawn("nb-ball", 0, 0, ball, '0 0 64', world, ball.team, ball, waypointsprite_attachedforcarrier, FALSE, RADARICON_FLAGCARRIER, BALL_SPRITECOLOR); // no health bar please
176         WaypointSprite_UpdateRule(ball.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
177
178         ball.owner.ballcarried = world;
179         ball.owner = world;
180 }
181
182 void InitBall(void)
183 {
184         if(gameover) return;
185         self.flags &~= FL_ONGROUND;
186         self.movetype = MOVETYPE_BOUNCE;
187         if(self.classname == "nexball_basketball")
188                 self.touch = basketball_touch;
189         else if(self.classname == "nexball_football")
190                 self.touch = football_touch;
191         self.cnt = 0;
192         self.think = ResetBall;
193         self.nextthink = time + autocvar_g_nexball_delay_idle + 3;
194         self.teamtime = 0;
195         self.pusher = world;
196         self.team = FALSE;
197         sound(self, CH_TRIGGER, self.noise1, VOL_BASE, ATTN_NORM);
198         WaypointSprite_Ping(self.waypointsprite_attachedforcarrier);
199         LogNB("init", world);
200 }
201
202 void ResetBall(void)
203 {
204         if(self.cnt < 2)        // step 1
205         {
206                 if(time == self.teamtime)
207                         bprint("The ", ColoredTeamName(self.team), " held the ball for too long.\n");
208                 self.touch = func_null;
209                 self.movetype = MOVETYPE_NOCLIP;
210                 self.velocity = '0 0 0'; // just in case?
211                 if(!self.cnt)
212                         LogNB("resetidle", world);
213                 self.cnt = 2;
214                 self.nextthink = time;
215         }
216         else if(self.cnt < 4)     // step 2 and 3
217         {
218 //              dprint("Step ", ftos(self.cnt), ": Calculated velocity: ", vtos(self.spawnorigin - self.origin), ", time: ", ftos(time), "\n");
219                 self.velocity = (self.spawnorigin - self.origin) * (self.cnt - 1); // 1 or 0.5 second movement
220                 self.nextthink = time + 0.5;
221                 self.cnt += 1;
222         }
223         else     // step 4
224         {
225 //              dprint("Step 4: time: ", ftos(time), "\n");
226                 if(vlen(self.origin - self.spawnorigin) > 10)  // should not happen anymore
227                         dprint("The ball moved too far away from its spawn origin.\nOffset: ",
228                                    vtos(self.origin - self.spawnorigin), " Velocity: ", vtos(self.velocity), "\n");
229                 self.velocity = '0 0 0';
230                 setorigin(self, self.spawnorigin); // make sure it's positioned correctly anyway
231                 self.movetype = MOVETYPE_NONE;
232                 self.think = InitBall;
233                 self.nextthink = max(time, game_starttime) + autocvar_g_nexball_delay_start;
234         }
235 }
236
237 void football_touch(void)
238 {
239         if(other.solid == SOLID_BSP)
240         {
241                 if(time > self.lastground + 0.1)
242                 {
243                         sound(self, CH_TRIGGER, self.noise, VOL_BASE, ATTN_NORM);
244                         self.lastground = time;
245                 }
246                 if(vlen(self.velocity) && !self.cnt)
247                         self.nextthink = time + autocvar_g_nexball_delay_idle;
248                 return;
249         }
250         if(other.classname != "player")
251                 return;
252         if(other.health < 1)
253                 return;
254         if(!self.cnt)
255                 self.nextthink = time + autocvar_g_nexball_delay_idle;
256
257         self.pusher = other;
258         self.team = other.team;
259
260         if(autocvar_g_nexball_football_physics == -1)   // MrBougo try 1, before decompiling Rev's original
261         {
262                 if(vlen(other.velocity))
263                         self.velocity = other.velocity * 1.5 + '0 0 1' * autocvar_g_nexball_football_boost_up;
264         }
265         else if(autocvar_g_nexball_football_physics == 1)         // MrBougo's modded Rev style: partially independant of the height of the aiming point
266         {
267                 makevectors(other.v_angle);
268                 self.velocity = other.velocity + v_forward * autocvar_g_nexball_football_boost_forward + '0 0 1' * autocvar_g_nexball_football_boost_up;
269         }
270         else if(autocvar_g_nexball_football_physics == 2)         // 2nd mod try: totally independant. Really playable!
271         {
272                 makevectors(other.v_angle_y * '0 1 0');
273                 self.velocity = other.velocity + v_forward * autocvar_g_nexball_football_boost_forward + v_up * autocvar_g_nexball_football_boost_up;
274         }
275         else     // Revenant's original style (from the original mod's disassembly, acknowledged by Revenant)
276         {
277                 makevectors(other.v_angle);
278                 self.velocity = other.velocity + v_forward * autocvar_g_nexball_football_boost_forward + v_up * autocvar_g_nexball_football_boost_up;
279         }
280         self.avelocity = -250 * v_forward;  // maybe there is a way to make it look better?
281 }
282
283 void basketball_touch(void)
284 {
285         if(other.ballcarried)
286         {
287                 football_touch();
288                 return;
289         }
290         if(!self.cnt && other.classname == "player" && (other != self.nb_dropper || time > self.nb_droptime + autocvar_g_nexball_delay_collect))
291         {
292                 if(other.health <= 0)
293                         return;
294                 LogNB("caught", other);
295                 GiveBall(other, self);
296         }
297         else if(other.solid == SOLID_BSP)
298         {
299                 sound(self, CH_TRIGGER, self.noise, VOL_BASE, ATTN_NORM);
300                 if(vlen(self.velocity) && !self.cnt)
301                         self.nextthink = min(time + autocvar_g_nexball_delay_idle, self.teamtime);
302         }
303 }
304
305 void GoalTouch(void)
306 {
307         entity ball;
308         float isclient, pscore, otherteam;
309         string pname;
310
311         if(gameover) return;
312         if((self.spawnflags & GOAL_TOUCHPLAYER) && other.ballcarried)
313                 ball = other.ballcarried;
314         else
315                 ball = other;
316         if(ball.classname != "nexball_basketball")
317                 if(ball.classname != "nexball_football")
318                         return;
319         if((!ball.pusher && self.team != GOAL_OUT) || ball.cnt)
320                 return;
321         EXACTTRIGGER_TOUCH;
322
323
324         if(nb_teams == 2)
325                 otherteam = OtherTeam(ball.team);
326         else
327                 otherteam = 0;
328
329         if((isclient = ball.pusher.flags & FL_CLIENT))
330                 pname = ball.pusher.netname;
331         else
332                 pname = "Someone (?)";
333
334         if(ball.team == self.team)               //owngoal (regular goals)
335         {
336                 LogNB("owngoal", ball.pusher);
337                 bprint("Boo! ", pname, "^7 scored a goal against their own team!\n");
338                 pscore = -1;
339         }
340         else if(self.team == GOAL_FAULT)
341         {
342                 LogNB("fault", ball.pusher);
343                 if(nb_teams == 2)
344                         bprint(ColoredTeamName(otherteam), " gets a point due to ", pname, "^7's silliness.\n");
345                 else
346                         bprint(ColoredTeamName(ball.team), " loses a point due to ", pname, "^7's silliness.\n");
347                 pscore = -1;
348         }
349         else if(self.team == GOAL_OUT)
350         {
351                 LogNB("out", ball.pusher);
352                 if((self.spawnflags & GOAL_TOUCHPLAYER) && ball.owner)
353                         bprint(pname, "^7 went out of bounds.\n");
354                 else
355                         bprint("The ball was returned.\n");
356                 pscore = 0;
357         }
358         else                                                       //score
359         {
360                 LogNB(strcat("goal:", ftos(self.team)), ball.pusher);
361                 bprint("Goaaaaal! ", pname, "^7 scored a point for the ", ColoredTeamName(ball.team), ".\n");
362                 pscore = 1;
363         }
364
365         sound(ball, CH_TRIGGER, self.noise, VOL_BASE, ATTN_NONE);
366
367         if(ball.team && pscore)
368         {
369                 if(nb_teams == 2 && pscore < 0)
370                         TeamScore_AddToTeam(otherteam, ST_NEXBALL_GOALS, -pscore);
371                 else
372                         TeamScore_AddToTeam(ball.team, ST_NEXBALL_GOALS, pscore);
373         }
374         if(isclient)
375         {
376                 if(pscore > 0)
377                         PlayerScore_Add(ball.pusher, SP_NEXBALL_GOALS, pscore);
378                 else if(pscore < 0)
379                         PlayerScore_Add(ball.pusher, SP_NEXBALL_FAULTS, -pscore);
380         }
381
382         if(ball.owner)  // Happens on spawnflag GOAL_TOUCHPLAYER
383                 DropBall(ball, ball.owner.origin, ball.owner.velocity);
384
385         WaypointSprite_Ping(ball.waypointsprite_attachedforcarrier);
386
387         ball.cnt = 1;
388         ball.think = ResetBall;
389         if(ball.classname == "nexball_basketball")
390                 ball.touch = football_touch; // better than func_null: football control until the ball gets reset
391         ball.nextthink = time + autocvar_g_nexball_delay_goal * (self.team != GOAL_OUT);
392 }
393
394 //=======================//
395 //         team ents       //
396 //=======================//
397 void spawnfunc_nexball_team(void)
398 {
399         if(!g_nexball)
400         {
401                 remove(self);
402                 return;
403         }
404         self.team = self.cnt + 1;
405 }
406
407 void nb_spawnteam(string teamname, float teamcolor)
408 {
409         dprint("^2spawned team ", teamname, "\n");
410         entity e;
411         e = spawn();
412         e.classname = "nexball_team";
413         e.netname = teamname;
414         e.cnt = teamcolor;
415         e.team = e.cnt + 1;
416         nb_teams += 1;
417 }
418
419 void nb_spawnteams(void)
420 {
421         float t_r = 0, t_b = 0, t_y = 0, t_p = 0;
422         entity e;
423         for(e = world; (e = find(e, classname, "nexball_goal"));)
424         {
425                 switch(e.team)
426                 {
427                 case COLOR_TEAM1:
428                         if(!t_r)
429                         {
430                                 nb_spawnteam("Red", e.team-1)   ;
431                                 t_r = 1;
432                         }
433                         break;
434                 case COLOR_TEAM2:
435                         if(!t_b)
436                         {
437                                 nb_spawnteam("Blue", e.team-1)  ;
438                                 t_b = 1;
439                         }
440                         break;
441                 case COLOR_TEAM3:
442                         if(!t_y)
443                         {
444                                 nb_spawnteam("Yellow", e.team-1);
445                                 t_y = 1;
446                         }
447                         break;
448                 case COLOR_TEAM4:
449                         if(!t_p)
450                         {
451                                 nb_spawnteam("Pink", e.team-1)  ;
452                                 t_p = 1;
453                         }
454                         break;
455                 }
456         }
457 }
458
459 void nb_delayedinit(void)
460 {
461         if(find(world, classname, "nexball_team") == world)
462                 nb_spawnteams();
463         ScoreRules_nexball(nb_teams);
464 }
465
466
467 //=======================//
468 //        spawnfuncs       //
469 //=======================//
470
471 void SpawnBall(void)
472 {
473         if(!g_nexball)
474         {
475                 remove(self);
476                 return;
477         }
478
479 //      balls += 4; // using the remaining bits to count balls will leave more than the max edict count, so it's fine
480
481         if(!self.model)
482         {
483                 self.model = "models/nexball/ball.md3";
484                 self.scale = 1.3;
485         }
486
487         precache_model(self.model);
488         setmodel(self, self.model);
489         setsize(self, BALL_MINS, BALL_MAXS);
490         ball_scale = self.scale;
491
492         relocate_nexball();
493         self.spawnorigin = self.origin;
494
495         self.effects = self.effects | EF_LOWPRECISION;
496
497         if(cvar(strcat("g_", self.classname, "_trail")))  //nexball_basketball :p
498         {
499                 self.glow_color = autocvar_g_nexball_trail_color;
500                 self.glow_trail = TRUE;
501         }
502
503         self.movetype = MOVETYPE_FLY;
504
505         if(!autocvar_g_nexball_sound_bounce)
506                 self.noise = "";
507         else if(!self.noise)
508                 self.noise = "sound/nexball/bounce.wav";
509         //bounce sound placeholder (FIXME)
510         if(!self.noise1)
511                 self.noise1 = "sound/nexball/drop.wav";
512         //ball drop sound placeholder (FIXME)
513         if(!self.noise2)
514                 self.noise2 = "sound/nexball/steal.wav";
515         //stealing sound placeholder (FIXME)
516         if(self.noise) precache_sound(self.noise);
517         precache_sound(self.noise1);
518         precache_sound(self.noise2);
519
520         WaypointSprite_AttachCarrier("nb-ball", self, RADARICON_FLAGCARRIER, BALL_SPRITECOLOR); // the ball's team is not set yet, no rule update needed
521
522         self.reset = ball_restart;
523         self.think = InitBall;
524         self.nextthink = game_starttime + autocvar_g_nexball_delay_start;
525 }
526
527 void spawnfunc_nexball_basketball(void)
528 {
529         nexball_mode |= NBM_BASKETBALL;
530         self.classname = "nexball_basketball";
531         if not(balls & BALL_BASKET)
532         {
533                 /*
534                 CVTOV(g_nexball_basketball_effects_default);
535                 CVTOV(g_nexball_basketball_delay_hold);
536                 CVTOV(g_nexball_basketball_delay_hold_forteam);
537                 CVTOV(g_nexball_basketball_teamsteal);
538                 */
539                 autocvar_g_nexball_basketball_effects_default = autocvar_g_nexball_basketball_effects_default & BALL_EFFECTMASK;
540         }
541         if(!self.effects)
542                 self.effects = autocvar_g_nexball_basketball_effects_default;
543         self.solid = SOLID_TRIGGER;
544         balls |= BALL_BASKET;
545         self.bouncefactor = autocvar_g_nexball_basketball_bouncefactor;
546         self.bouncestop = autocvar_g_nexball_basketball_bouncestop;
547         SpawnBall();
548 }
549
550 void spawnfunc_nexball_football(void)
551 {
552         nexball_mode |= NBM_FOOTBALL;
553         self.classname = "nexball_football";
554         self.solid = SOLID_TRIGGER;
555         balls |= BALL_FOOT;
556         self.bouncefactor = autocvar_g_nexball_football_bouncefactor;
557         self.bouncestop = autocvar_g_nexball_football_bouncestop;
558         SpawnBall();
559 }
560
561 void SpawnGoal(void)
562 {
563         if(!g_nexball)
564         {
565                 remove(self);
566                 return;
567         }
568         EXACTTRIGGER_INIT;
569         self.classname = "nexball_goal";
570         if(!self.noise)
571                 self.noise = "ctf/respawn.wav";
572         precache_sound(self.noise);
573         self.touch = GoalTouch;
574 }
575
576 void spawnfunc_nexball_redgoal(void)
577 {
578         self.team = COLOR_TEAM1;
579         SpawnGoal();
580 }
581 void spawnfunc_nexball_bluegoal(void)
582 {
583         self.team = COLOR_TEAM2;
584         SpawnGoal();
585 }
586 void spawnfunc_nexball_yellowgoal(void)
587 {
588         self.team = COLOR_TEAM3;
589         SpawnGoal();
590 }
591 void spawnfunc_nexball_pinkgoal(void)
592 {
593         self.team = COLOR_TEAM4;
594         SpawnGoal();
595 }
596
597 void spawnfunc_nexball_fault(void)
598 {
599         self.team = GOAL_FAULT;
600         if(!self.noise)
601                 self.noise = "misc/typehit.wav";
602         SpawnGoal();
603 }
604
605 void spawnfunc_nexball_out(void)
606 {
607         self.team = GOAL_OUT;
608         if(!self.noise)
609                 self.noise = "misc/typehit.wav";
610         SpawnGoal();
611 }
612
613 //
614 //Spawnfuncs preserved for compatibility
615 //
616
617 void spawnfunc_ball(void)
618 {
619         spawnfunc_nexball_football();
620 }
621 void spawnfunc_ball_football(void)
622 {
623         spawnfunc_nexball_football();
624 }
625 void spawnfunc_ball_basketball(void)
626 {
627         spawnfunc_nexball_basketball();
628 }
629 // The "red goal" is defended by blue team. A ball in there counts as a point for red.
630 void spawnfunc_ball_redgoal(void)
631 {
632         spawnfunc_nexball_bluegoal();   // I blame Revenant
633 }
634 void spawnfunc_ball_bluegoal(void)
635 {
636         spawnfunc_nexball_redgoal();    // but he didn't mean to cause trouble :p
637 }
638 void spawnfunc_ball_fault(void)
639 {
640         spawnfunc_nexball_fault();
641 }
642 void spawnfunc_ball_bound(void)
643 {
644         spawnfunc_nexball_out();
645 }
646
647 //=======================//
648 //        Weapon code     //
649 //=======================//
650
651
652 void W_Nexball_Think()
653 {
654         //dprint("W_Nexball_Think\n");
655         //vector new_dir = steerlib_arrive(self.enemy.origin, 2500);
656         vector new_dir = normalize(self.enemy.origin + '0 0 50' - self.origin);
657         vector old_dir = normalize(self.velocity);       
658         float _speed = vlen(self.velocity);     
659         vector new_vel = normalize(old_dir + (new_dir * autocvar_g_nexball_safepass_turnrate)) * _speed;
660         //vector new_vel = (new_dir * autocvar_g_nexball_safepass_turnrate
661         
662         self.velocity = new_vel;
663         
664         self.nextthink = time;
665 }
666
667 void W_Nexball_Touch(void)
668 {
669         entity ball, attacker;
670         attacker = self.owner;
671         //self.think = func_null;
672         //self.enemy = world;
673         
674         PROJECTILE_TOUCH;
675         if(attacker.team != other.team || autocvar_g_nexball_basketball_teamsteal)
676                 if((ball = other.ballcarried) && (attacker.classname == "player"))
677                 {
678                         other.velocity = other.velocity + normalize(self.velocity) * other.damageforcescale * autocvar_g_balance_nexball_secondary_force;
679                         other.flags &~= FL_ONGROUND;
680                         if(!attacker.ballcarried)
681                         {
682                                 LogNB("stole", attacker);
683                                 sound(other, CH_TRIGGER, ball.noise2, VOL_BASE, ATTN_NORM);
684
685                                 if(attacker.team == other.team && time > attacker.teamkill_complain)
686                                 {
687                                         attacker.teamkill_complain = time + 5;
688                                         attacker.teamkill_soundtime = time + 0.4;
689                                         attacker.teamkill_soundsource = other;
690                                 }
691
692                                 GiveBall(attacker, other.ballcarried);
693                         }
694                 }
695         remove(self);
696 }
697
698 void W_Nexball_Attack(float t)
699 {
700         entity ball;
701         float mul, mi, ma;
702         if(!(ball = self.ballcarried))
703                 return;
704
705         W_SetupShot(self, FALSE, 4, "nexball/shoot1.wav", CH_WEAPON_A, 0);
706         tracebox(w_shotorg, BALL_MINS, BALL_MAXS, w_shotorg, MOVE_WORLDONLY, world);
707         if(trace_startsolid)
708         {
709                 if(self.metertime)
710                         self.metertime = 0; // Shot failed, hide the power meter
711                 return;
712         }
713
714         //Calculate multiplier
715         if(t < 0)
716                 mul = 1;
717         else
718         {
719                 mi = autocvar_g_nexball_basketball_meter_minpower;
720                 ma = max(mi, autocvar_g_nexball_basketball_meter_maxpower); // avoid confusion
721                 //One triangle wave period with 1 as max
722                 mul = 2 * mod(t, g_nexball_meter_period) / g_nexball_meter_period;
723                 if(mul > 1)
724                         mul = 2 - mul;
725                 mul = mi + (ma - mi) * mul; // range from the minimal power to the maximal power
726         }
727         
728         DropBall(ball, w_shotorg, W_CalculateProjectileVelocity(self.velocity, w_shotdir * autocvar_g_balance_nexball_primary_speed * mul, FALSE));
729         
730
731         //TODO: use the speed_up cvar too ??
732 }
733
734 void W_Nexball_Attack2(void)
735 {
736         if(self.ballcarried.enemy)
737         {
738                 entity _ball = self.ballcarried;
739                 W_SetupShot(self, FALSE, 4, "nexball/shoot1.wav", CH_WEAPON_A, 0);
740                 DropBall(_ball, w_shotorg, trigger_push_calculatevelocity(_ball.origin, _ball.enemy, 32));
741                 _ball.think = W_Nexball_Think;
742                 _ball.nextthink = time;
743                 return;
744         }
745         
746         if(!autocvar_g_nexball_tackling)
747                 return;
748         
749         entity missile;
750         if(!(balls & BALL_BASKET))
751                 return;
752         W_SetupShot(self, FALSE, 2, "nexball/shoot2.wav", CH_WEAPON_A, 0);
753 //      pointparticles(particleeffectnum("grenadelauncher_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
754         missile = spawn();
755
756         missile.owner = self;
757         missile.classname = "ballstealer";
758
759         missile.movetype = MOVETYPE_FLY;
760         PROJECTILE_MAKETRIGGER(missile);
761
762         setmodel(missile, "models/elaser.mdl");  // precision set below
763         setsize(missile, '0 0 0', '0 0 0');
764         setorigin(missile, w_shotorg);
765
766         W_SetupProjectileVelocity(missile, autocvar_g_balance_nexball_secondary_speed, 0);
767         missile.angles = vectoangles(missile.velocity);
768         missile.touch = W_Nexball_Touch;
769         missile.think = SUB_Remove;
770         missile.nextthink = time + autocvar_g_balance_nexball_secondary_lifetime; //FIXME: use a distance instead?
771
772         missile.effects = EF_BRIGHTFIELD | EF_LOWPRECISION;
773         missile.flags = FL_PROJECTILE;
774 }
775
776 float ball_customize()
777 {
778         if(!self.owner)
779         {
780                 self.effects &~= EF_FLAME;
781                 self.scale = 1;
782                 self.customizeentityforclient = func_null;
783                 return TRUE;
784         }               
785         
786         if(other == self.owner)
787         {
788                 self.scale = autocvar_g_nexball_viewmodel_scale;
789                 if(self.enemy)
790                         self.effects |= EF_FLAME;
791                 else
792                         self.effects &~= EF_FLAME;
793         }       
794         else
795         {
796                 self.effects &~= EF_FLAME;
797                 self.scale = 1;
798         }
799                 
800         return TRUE;
801 }
802
803 float w_nexball_weapon(float req)
804 {
805         if(req == WR_THINK)
806         {
807                 if(self.BUTTON_ATCK)
808                         if(weapon_prepareattack(0, autocvar_g_balance_nexball_primary_refire))
809                                 if(autocvar_g_nexball_basketball_meter)
810                                 {
811                                         if(self.ballcarried && !self.metertime)
812                                                 self.metertime = time;
813                                         else
814                                                 weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_nexball_primary_animtime, w_ready);
815                                 }
816                                 else
817                                 {
818                                         W_Nexball_Attack(-1);
819                                         weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_nexball_primary_animtime, w_ready);
820                                 }
821                 if(self.BUTTON_ATCK2)
822                         if(weapon_prepareattack(1, autocvar_g_balance_nexball_secondary_refire))
823                         {
824                                 W_Nexball_Attack2();
825                                 weapon_thinkf(WFRAME_FIRE2, autocvar_g_balance_nexball_secondary_animtime, w_ready);
826                         }
827
828                 if(!self.BUTTON_ATCK && self.metertime && self.ballcarried)
829                 {
830                         W_Nexball_Attack(time - self.metertime);
831                         // DropBall or stealing will set metertime back to 0
832                         weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_nexball_primary_animtime, w_ready);
833                 }
834         }
835         else if(req == WR_PRECACHE)
836         {
837                 precache_model("models/weapons/g_porto.md3");
838                 precache_model("models/weapons/v_porto.md3");
839                 precache_model("models/weapons/h_porto.iqm");
840                 precache_model("models/elaser.mdl");
841                 precache_sound("nexball/shoot1.wav");
842                 precache_sound("nexball/shoot2.wav");
843                 precache_sound("misc/typehit.wav");
844         }
845         else if(req == WR_SETUP)
846                 weapon_setup(WEP_PORTO);
847         else if(req == WR_SUICIDEMESSAGE)
848         {
849                 w_deathtypestring = "is a weirdo";
850         }
851         else if(req == WR_KILLMESSAGE)
852         {
853                 w_deathtypestring = "got killed by #'s black magic";
854         }
855         // No need to check WR_CHECKAMMO* or WR_AIM, it should always return TRUE
856         return TRUE;
857 }
858
859 MUTATOR_HOOKFUNCTION(nexball_BallDrop)
860 {
861         if(self.ballcarried && g_nexball)
862                 DropBall(self.ballcarried, self.origin, self.velocity);
863
864         return 0;
865 }
866
867 MUTATOR_HOOKFUNCTION(nexball_BuildMutatorsString)
868 {
869         ret_string = strcat(ret_string, ":NB");
870         return 0;
871 }
872
873 MUTATOR_HOOKFUNCTION(nexball_BuildMutatorsPrettyString)
874 {
875         ret_string = strcat(ret_string, ", NexBall");
876         return 0;
877 }
878
879 MUTATOR_HOOKFUNCTION(nexball_PlayerPreThink)
880 {
881         makevectors(self.v_angle);
882         if(nexball_mode & NBM_BASKETBALL)
883         {               
884                 if(self.ballcarried)
885                 {
886                         // 'view ball'
887                         self.ballcarried.velocity = self.velocity;                      
888                         self.ballcarried.customizeentityforclient = ball_customize;
889                         
890                         setorigin(self.ballcarried, self.origin + self.view_ofs + 
891                                           v_forward * autocvar_g_nexball_viewmodel_offset_x + 
892                                           v_right * autocvar_g_nexball_viewmodel_offset_y + 
893                                           v_up * autocvar_g_nexball_viewmodel_offset_z);        
894                                           
895                         // 'safe passing'
896                         if(autocvar_g_nexball_safepass_maxdist)
897                         {
898                                 if(self.ballcarried.wait < time && self.ballcarried.enemy)
899                                 {
900                                         //centerprint(self, sprintf("Lost lock on %s", self.ballcarried.enemy.netname));
901                                         self.ballcarried.enemy = world;
902                                 }
903                                         
904                                 
905                                 //tracebox(self.origin + self.view_ofs, '-2 -2 -2', '2 2 2', self.origin + self.view_ofs + v_forward * autocvar_g_nexball_safepass_maxdist);
906                                 crosshair_trace(self);
907                                 if( trace_ent && 
908                                         trace_ent.flags & FL_CLIENT &&
909                                         trace_ent.deadflag == DEAD_NO &&
910                                         trace_ent.team == self.team &&
911                                         vlen(trace_ent.origin - self.origin) <= autocvar_g_nexball_safepass_maxdist )
912                                 {
913                                         
914                                         //if(self.ballcarried.enemy != trace_ent)
915                                         //      centerprint(self, sprintf("Locked to %s", trace_ent.netname));
916                                         self.ballcarried.enemy = trace_ent;
917                                         self.ballcarried.wait = time + autocvar_g_nexball_safepass_holdtime;
918                                         
919                                         
920                                 }
921                         }
922                 }
923                 else
924                 {                       
925                         if(!WEPSET_EMPTY_E(self.weaponentity))
926                         {
927                                 WEPSET_COPY_EE(self, self.weaponentity);
928                                 weapon_action(WEP_PORTO, WR_RESETPLAYER);
929                                 self.switchweapon = self.weaponentity.switchweapon;
930                                 W_SwitchWeapon(self.switchweapon);
931                                 
932                 WEPSET_CLEAR_E(self.weaponentity);
933                         }
934                 }
935                 
936         }
937         return FALSE;
938 }
939
940 MUTATOR_HOOKFUNCTION(nexball_PlayerSpawn)
941 {       
942         WEPSET_CLEAR_E(self.weaponentity);
943         
944         if(nexball_mode & NBM_BASKETBALL)
945                 WEPSET_OR_EW(self, WEP_PORTO);
946         else
947                 WEPSET_CLEAR_E(self);
948
949         return FALSE;
950 }
951
952 MUTATOR_DEFINITION(gamemode_nexball)
953 {
954         MUTATOR_HOOK(PlayerDies, nexball_BallDrop, CBC_ORDER_ANY);
955         MUTATOR_HOOK(MakePlayerObserver, nexball_BallDrop, CBC_ORDER_ANY);
956         MUTATOR_HOOK(ClientDisconnect, nexball_BallDrop, CBC_ORDER_ANY);
957         MUTATOR_HOOK(BuildMutatorsPrettyString, nexball_BuildMutatorsPrettyString, CBC_ORDER_ANY);
958         MUTATOR_HOOK(BuildMutatorsString, nexball_BuildMutatorsString, CBC_ORDER_ANY);
959         MUTATOR_HOOK(PlayerSpawn, nexball_PlayerSpawn, CBC_ORDER_ANY);
960         MUTATOR_HOOK(PlayerPreThink, nexball_PlayerPreThink, CBC_ORDER_ANY);
961
962         MUTATOR_ONADD
963         {
964                 g_nexball_meter_period = autocvar_g_nexball_meter_period;
965                 if(g_nexball_meter_period <= 0)
966                         g_nexball_meter_period = 2; // avoid division by zero etc. due to silly users
967                 g_nexball_meter_period = rint(g_nexball_meter_period * 32) / 32; //Round to 1/32ths to send as a byte multiplied by 32
968                 addstat(STAT_NB_METERSTART, AS_FLOAT, metertime);
969
970                 // General settings
971                 /*
972                 CVTOV(g_nexball_football_boost_forward);   //100
973                 CVTOV(g_nexball_football_boost_up);             //200
974                 CVTOV(g_nexball_delay_idle);                       //10
975                 CVTOV(g_nexball_football_physics);               //0
976                 */
977                 radar_showennemies = autocvar_g_nexball_radar_showallplayers;
978
979                 InitializeEntity(world, nb_delayedinit, INITPRIO_GAMETYPE);
980         }
981
982         MUTATOR_ONROLLBACK_OR_REMOVE
983         {
984                 // we actually cannot roll back nb_delayedinit here
985                 // BUT: we don't need to! If this gets called, adding always
986                 // succeeds.
987         }
988
989         MUTATOR_ONREMOVE
990         {
991                 print("This is a game type and it cannot be removed at runtime.");
992                 return -1;
993         }
994
995         return 0;
996 }