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