Merge remote branch 'origin/master' into tzork/gm_nexball
[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.dropperid = plyr.playerid;
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 = SUB_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     self.weaponentity.weapons = self.weapons;
143     self.weaponentity.switchweapon = self.weapon;
144     self.weapons = W_WeaponBit(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.ctf_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 = SUB_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.playerid != self.dropperid || time > self.ctf_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
327         if((isclient = ball.pusher.flags & FL_CLIENT))
328                 pname = ball.pusher.netname;
329         else
330                 pname = "Someone (?)";
331
332         if(ball.team == self.team)         //owngoal (regular goals)
333         {
334                 LogNB("owngoal", ball.pusher);
335                 bprint("Boo! ", pname, "^7 scored a goal against their own team!\n");
336                 pscore = -1;
337         }
338         else if(self.team == GOAL_FAULT)
339         {
340                 LogNB("fault", ball.pusher);
341                 if(nb_teams == 2)
342                         bprint(ColoredTeamName(otherteam), " gets a point due to ", pname, "^7's silliness.\n");
343                 else
344                         bprint(ColoredTeamName(ball.team), " loses a point due to ", pname, "^7's silliness.\n");
345                 pscore = -1;
346         }
347         else if(self.team == GOAL_OUT)
348         {
349                 LogNB("out", ball.pusher);
350                 if((self.spawnflags & GOAL_TOUCHPLAYER) && ball.owner)
351                         bprint(pname, "^7 went out of bounds.\n");
352                 else
353                         bprint("The ball was returned.\n");
354                 pscore = 0;
355         }
356         else                               //score
357         {
358                 LogNB(strcat("goal:", ftos(self.team)), ball.pusher);
359                 bprint("Goaaaaal! ", pname, "^7 scored a point for the ", ColoredTeamName(ball.team), ".\n");
360                 pscore = 1;
361         }
362
363         sound(ball, CH_TRIGGER, self.noise, VOL_BASE, ATTN_NONE);
364
365         if(ball.team && pscore)
366         {
367                 if(nb_teams == 2 && pscore < 0)
368                         TeamScore_AddToTeam(otherteam, ST_NEXBALL_GOALS, -pscore);
369                 else
370                         TeamScore_AddToTeam(ball.team, ST_NEXBALL_GOALS, pscore);
371         }
372         if(isclient)
373         {
374                 if(pscore > 0)
375                         PlayerScore_Add(ball.pusher, SP_NEXBALL_GOALS, pscore);
376                 else if(pscore < 0)
377                         PlayerScore_Add(ball.pusher, SP_NEXBALL_FAULTS, -pscore);
378         }
379
380         if(ball.owner)  // Happens on spawnflag GOAL_TOUCHPLAYER
381                 DropBall(ball, ball.owner.origin, ball.owner.velocity);
382
383         WaypointSprite_Ping(ball.waypointsprite_attachedforcarrier);
384
385         ball.cnt = 1;
386         ball.think = ResetBall;
387         if(ball.classname == "nexball_basketball")
388                 ball.touch = football_touch; // better than SUB_Null: football control until the ball gets reset
389         ball.nextthink = time + autocvar_g_nexball_delay_goal * (self.team != GOAL_OUT);
390 }
391
392 //=======================//
393 //       team ents       //
394 //=======================//
395 void spawnfunc_nexball_team(void)
396 {
397         if(!g_nexball)
398         {
399                 remove(self);
400                 return;
401         }
402         self.team = self.cnt + 1;
403 }
404
405 void nb_spawnteam(string teamname, float teamcolor)
406 {
407         dprint("^2spawned team ", teamname, "\n");
408         entity e;
409         e = spawn();
410         e.classname = "nexball_team";
411         e.netname = teamname;
412         e.cnt = teamcolor;
413         e.team = e.cnt + 1;
414         nb_teams += 1;
415 }
416
417 void nb_spawnteams(void)
418 {
419         float t_r, t_b, t_y, t_p;
420         entity e;
421         for(e = world; (e = find(e, classname, "nexball_goal"));)
422         {
423                 switch(e.team)
424                 {
425                 case COLOR_TEAM1:
426                         if(!t_r)
427                         {
428                                 nb_spawnteam("Red", e.team-1)   ;
429                                 t_r = 1;
430                         }
431                         break;
432                 case COLOR_TEAM2:
433                         if(!t_b)
434                         {
435                                 nb_spawnteam("Blue", e.team-1)  ;
436                                 t_b = 1;
437                         }
438                         break;
439                 case COLOR_TEAM3:
440                         if(!t_y)
441                         {
442                                 nb_spawnteam("Yellow", e.team-1);
443                                 t_y = 1;
444                         }
445                         break;
446                 case COLOR_TEAM4:
447                         if(!t_p)
448                         {
449                                 nb_spawnteam("Pink", e.team-1)  ;
450                                 t_p = 1;
451                         }
452                         break;
453                 }
454         }
455 }
456
457 void nb_delayedinit(void)
458 {
459         if(find(world, classname, "nexball_team") == world)
460                 nb_spawnteams();
461         ScoreRules_nexball(nb_teams);
462 }
463
464
465 //=======================//
466 //      spawnfuncs       //
467 //=======================//
468
469 void SpawnBall(void)
470 {
471         if(!g_nexball)
472         {
473                 remove(self);
474                 return;
475         }
476
477 //      balls += 4; // using the remaining bits to count balls will leave more than the max edict count, so it's fine
478
479         if(!self.model)
480         {
481                 self.model = "models/nexball/ball.md3";
482                 self.scale = 1.3;
483         }
484
485         precache_model(self.model);
486         setmodel(self, self.model);
487         setsize(self, BALL_MINS, BALL_MAXS);
488         ball_scale = self.scale;
489
490         relocate_nexball();
491         self.spawnorigin = self.origin;
492
493         self.effects = self.effects | EF_LOWPRECISION;
494
495         if(cvar(strcat("g_", self.classname, "_trail")))  //nexball_basketball :p
496         {
497                 self.glow_color = autocvar_g_nexball_trail_color;
498                 self.glow_trail = TRUE;
499         }
500
501         self.movetype = MOVETYPE_FLY;
502
503         if(!autocvar_g_nexball_sound_bounce)
504                 self.noise = "";
505         else if(!self.noise)
506                 self.noise = "sound/nexball/bounce.wav";
507         //bounce sound placeholder (FIXME)
508         if(!self.noise1)
509                 self.noise1 = "sound/nexball/drop.wav";
510         //ball drop sound placeholder (FIXME)
511         if(!self.noise2)
512                 self.noise2 = "sound/nexball/steal.wav";
513         //stealing sound placeholder (FIXME)
514         if(self.noise) precache_sound(self.noise);
515         precache_sound(self.noise1);
516         precache_sound(self.noise2);
517
518         WaypointSprite_AttachCarrier("nb-ball", self, RADARICON_FLAGCARRIER, BALL_SPRITECOLOR); // the ball's team is not set yet, no rule update needed
519
520         self.reset = ball_restart;
521         self.think = InitBall;
522         self.nextthink = game_starttime + autocvar_g_nexball_delay_start;
523 }
524
525 void spawnfunc_nexball_basketball(void)
526 {
527     nexball_mode |= NBM_BASKETBALL;
528         self.classname = "nexball_basketball";
529         if not(balls & BALL_BASKET)
530         {
531                 /*
532                 CVTOV(g_nexball_basketball_effects_default);
533                 CVTOV(g_nexball_basketball_delay_hold);
534                 CVTOV(g_nexball_basketball_delay_hold_forteam);
535                 CVTOV(g_nexball_basketball_teamsteal);
536                 */
537                 autocvar_g_nexball_basketball_effects_default = autocvar_g_nexball_basketball_effects_default & BALL_EFFECTMASK;
538         }
539         if(!self.effects)
540                 self.effects = autocvar_g_nexball_basketball_effects_default;
541         self.solid = SOLID_TRIGGER;
542         balls |= BALL_BASKET;
543         self.bouncefactor = autocvar_g_nexball_basketball_bouncefactor;
544         self.bouncestop = autocvar_g_nexball_basketball_bouncestop;
545         SpawnBall();
546 }
547
548 void spawnfunc_nexball_football(void)
549 {
550     nexball_mode |= NBM_FOOTBALL;
551         self.classname = "nexball_football";
552         self.solid = SOLID_TRIGGER;
553         balls |= BALL_FOOT;
554         self.bouncefactor = autocvar_g_nexball_football_bouncefactor;
555         self.bouncestop = autocvar_g_nexball_football_bouncestop;
556         SpawnBall();
557 }
558
559 void SpawnGoal(void)
560 {
561         if(!g_nexball)
562         {
563                 remove(self);
564                 return;
565         }
566         EXACTTRIGGER_INIT;
567         self.classname = "nexball_goal";
568         if(!self.noise)
569                 self.noise = "ctf/respawn.wav";
570         precache_sound(self.noise);
571         self.touch = GoalTouch;
572 }
573
574 void spawnfunc_nexball_redgoal(void)
575 {
576         self.team = COLOR_TEAM1;
577         SpawnGoal();
578 }
579 void spawnfunc_nexball_bluegoal(void)
580 {
581         self.team = COLOR_TEAM2;
582         SpawnGoal();
583 }
584 void spawnfunc_nexball_yellowgoal(void)
585 {
586         self.team = COLOR_TEAM3;
587         SpawnGoal();
588 }
589 void spawnfunc_nexball_pinkgoal(void)
590 {
591         self.team = COLOR_TEAM4;
592         SpawnGoal();
593 }
594
595 void spawnfunc_nexball_fault(void)
596 {
597         self.team = GOAL_FAULT;
598         if(!self.noise)
599                 self.noise = "misc/typehit.wav";
600         SpawnGoal();
601 }
602
603 void spawnfunc_nexball_out(void)
604 {
605         self.team = GOAL_OUT;
606         if(!self.noise)
607                 self.noise = "misc/typehit.wav";
608         SpawnGoal();
609 }
610
611 //
612 //Spawnfuncs preserved for compatibility
613 //
614
615 void spawnfunc_ball(void)
616 {
617         spawnfunc_nexball_football();
618 }
619 void spawnfunc_ball_football(void)
620 {
621         spawnfunc_nexball_football();
622 }
623 void spawnfunc_ball_basketball(void)
624 {
625         spawnfunc_nexball_basketball();
626 }
627 // The "red goal" is defended by blue team. A ball in there counts as a point for red.
628 void spawnfunc_ball_redgoal(void)
629 {
630         spawnfunc_nexball_bluegoal();    // I blame Revenant
631 }
632 void spawnfunc_ball_bluegoal(void)
633 {
634         spawnfunc_nexball_redgoal();    // but he didn't mean to cause trouble :p
635 }
636 void spawnfunc_ball_fault(void)
637 {
638         spawnfunc_nexball_fault();
639 }
640 void spawnfunc_ball_bound(void)
641 {
642         spawnfunc_nexball_out();
643 }
644
645 //=======================//
646 //      Weapon code      //
647 //=======================//
648
649
650 void W_Nexball_Think()
651 {
652     //dprint("W_Nexball_Think\n");
653     //vector new_dir = steerlib_arrive(self.enemy.origin, 2500);
654     vector new_dir = normalize(self.enemy.origin - self.origin);
655     vector old_dir = normalize(self.velocity);     
656     float _speed = vlen(self.velocity);    
657     vector new_vel = normalize(old_dir + (new_dir * autocvar_g_nexball_safepass_turnrate)) * _speed;
658     //vector new_vel = (new_dir * autocvar_g_nexball_safepass_turnrate
659     
660     self.velocity = new_vel;
661     
662     self.nextthink = time;
663 }
664
665 void W_Nexball_Touch(void)
666 {
667         entity ball, attacker;
668         attacker = self.owner;
669     //self.think = SUB_Null;
670     //self.enemy = world;
671     
672         PROJECTILE_TOUCH;
673         if(attacker.team != other.team || autocvar_g_nexball_basketball_teamsteal)
674                 if((ball = other.ballcarried) && (attacker.classname == "player"))
675                 {
676                         other.velocity = other.velocity + normalize(self.velocity) * other.damageforcescale * autocvar_g_balance_nexball_secondary_force;
677                         other.flags &~= FL_ONGROUND;
678                         if(!attacker.ballcarried)
679                         {
680                                 LogNB("stole", attacker);
681                                 sound(other, CH_TRIGGER, ball.noise2, VOL_BASE, ATTN_NORM);
682
683                                 if(attacker.team == other.team && time > attacker.teamkill_complain)
684                                 {
685                                         attacker.teamkill_complain = time + 5;
686                                         attacker.teamkill_soundtime = time + 0.4;
687                                         attacker.teamkill_soundsource = other;
688                                 }
689
690                                 GiveBall(attacker, other.ballcarried);
691                         }
692                 }
693         remove(self);
694 }
695
696 void W_Nexball_Attack(float t)
697 {
698         entity ball;
699         float mul, mi, ma;
700         if(!(ball = self.ballcarried))
701                 return;
702
703         W_SetupShot(self, FALSE, 4, "nexball/shoot1.wav", CH_WEAPON_A, 0);
704         tracebox(w_shotorg, BALL_MINS, BALL_MAXS, w_shotorg, MOVE_WORLDONLY, world);
705         if(trace_startsolid)
706         {
707                 if(self.metertime)
708                         self.metertime = 0; // Shot failed, hide the power meter
709                 return;
710         }
711
712         //Calculate multiplier
713         if(t < 0)
714                 mul = 1;
715         else
716         {
717                 mi = autocvar_g_nexball_basketball_meter_minpower;
718                 ma = max(mi, autocvar_g_nexball_basketball_meter_maxpower); // avoid confusion
719                 //One triangle wave period with 1 as max
720                 mul = 2 * mod(t, g_nexball_meter_period) / g_nexball_meter_period;
721                 if(mul > 1)
722                         mul = 2 - mul;
723                 mul = mi + (ma - mi) * mul; // range from the minimal power to the maximal power
724         }
725         
726     DropBall(ball, w_shotorg, W_CalculateProjectileVelocity(self.velocity, w_shotdir * autocvar_g_balance_nexball_primary_speed * mul, FALSE));
727         
728
729         //TODO: use the speed_up cvar too ??
730 }
731
732 void W_Nexball_Attack2(void)
733 {
734         if(self.ballcarried.enemy)
735         {
736             entity _ball = self.ballcarried;
737         W_SetupShot(self, FALSE, 4, "nexball/shoot1.wav", CH_WEAPON_A, 0);
738             DropBall(_ball, w_shotorg, trigger_push_calculatevelocity(_ball.origin, _ball.enemy, 32));
739         _ball.think = W_Nexball_Think;
740         _ball.nextthink = time;
741             return;
742         }
743     
744     if(!autocvar_g_nexball_tackling)
745         return;
746         
747         entity missile;
748         if(!(balls & BALL_BASKET))
749                 return;
750         W_SetupShot(self, FALSE, 2, "nexball/shoot2.wav", CH_WEAPON_A, 0);
751 //      pointparticles(particleeffectnum("grenadelauncher_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
752         missile = spawn();
753
754         missile.owner = self;
755         missile.classname = "ballstealer";
756
757         missile.movetype = MOVETYPE_FLY;
758         PROJECTILE_MAKETRIGGER(missile);
759
760         setmodel(missile, "models/elaser.mdl");  // precision set below
761         setsize(missile, '0 0 0', '0 0 0');
762         setorigin(missile, w_shotorg);
763
764         W_SetupProjectileVelocity(missile, autocvar_g_balance_nexball_secondary_speed, 0);
765         missile.angles = vectoangles(missile.velocity);
766         missile.touch = W_Nexball_Touch;
767         missile.think = SUB_Remove;
768         missile.nextthink = time + autocvar_g_balance_nexball_secondary_lifetime; //FIXME: use a distance instead?
769
770         missile.effects = EF_BRIGHTFIELD | EF_LOWPRECISION;
771         missile.flags = FL_PROJECTILE;
772 }
773
774 var const float() nullfunc;
775 float ball_customize()
776 {
777     if(!self.owner)
778     {
779         self.effects &~= EF_FLAME;
780         self.scale = 1;
781         self.customizeentityforclient = nullfunc;
782         return TRUE;
783     }        
784     
785     if(other == self.owner)
786     {
787         self.scale = autocvar_g_nexball_viewmodel_scale;
788         if(self.enemy)
789             self.effects |= EF_FLAME;
790         else
791             self.effects &~= EF_FLAME;
792     }    
793     else
794     {
795         self.effects &~= EF_FLAME;
796         self.scale = 1;
797     }
798         
799     return TRUE;
800 }
801
802 float w_nexball_weapon(float req)
803 {
804         if(req == WR_THINK)
805         {
806                 if(self.BUTTON_ATCK)
807                         if(weapon_prepareattack(0, autocvar_g_balance_nexball_primary_refire))
808                                 if(autocvar_g_nexball_basketball_meter)
809                                 {
810                                         if(self.ballcarried && !self.metertime)
811                                                 self.metertime = time;
812                                         else
813                                                 weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_nexball_primary_animtime, w_ready);
814                                 }
815                                 else
816                                 {
817                                         W_Nexball_Attack(-1);
818                                         weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_nexball_primary_animtime, w_ready);
819                                 }
820                 if(self.BUTTON_ATCK2)
821                         if(weapon_prepareattack(1, autocvar_g_balance_nexball_secondary_refire))
822                         {
823                                 W_Nexball_Attack2();
824                                 weapon_thinkf(WFRAME_FIRE2, autocvar_g_balance_nexball_secondary_animtime, w_ready);
825                         }
826
827                 if(!self.BUTTON_ATCK && self.metertime && self.ballcarried)
828                 {
829                         W_Nexball_Attack(time - self.metertime);
830                         // DropBall or stealing will set metertime back to 0
831                         weapon_thinkf(WFRAME_FIRE1, autocvar_g_balance_nexball_primary_animtime, w_ready);
832                 }
833         }
834         else if(req == WR_PRECACHE)
835         {
836                 precache_model("models/weapons/g_porto.md3");
837                 precache_model("models/weapons/v_porto.md3");
838                 precache_model("models/weapons/h_porto.iqm");
839                 precache_model("models/elaser.mdl");
840                 precache_sound("nexball/shoot1.wav");
841                 precache_sound("nexball/shoot2.wav");
842                 precache_sound("misc/typehit.wav");
843         }
844         else if(req == WR_SETUP)
845                 weapon_setup(WEP_PORTO);
846         else if(req == WR_SUICIDEMESSAGE)
847         {
848                 w_deathtypestring = "is a weirdo";
849         }
850         else if(req == WR_KILLMESSAGE)
851         {
852                 w_deathtypestring = "got killed by #'s black magic";
853         }
854         // No need to check WR_CHECKAMMO* or WR_AIM, it should always return TRUE
855         return TRUE;
856 }
857
858 MUTATOR_HOOKFUNCTION(nexball_BallDrop)
859 {
860         if(self.ballcarried && g_nexball)
861                 DropBall(self.ballcarried, self.origin, self.velocity);
862
863         return 0;
864 }
865
866 MUTATOR_HOOKFUNCTION(nexball_BuildMutatorsString)
867 {
868         ret_string = strcat(ret_string, ":NB");
869         return 0;
870 }
871
872 MUTATOR_HOOKFUNCTION(nexball_BuildMutatorsPrettyString)
873 {
874         ret_string = strcat(ret_string, ", NexBall");
875         return 0;
876 }
877
878 MUTATOR_HOOKFUNCTION(nexball_PlayerPreThink)
879 {
880     makevectors(self.v_angle);
881     if(nexball_mode & NBM_BASKETBALL)
882     {        
883         if(self.ballcarried)
884         {
885             // 'view ball'
886             self.ballcarried.velocity = self.velocity;            
887             self.ballcarried.customizeentityforclient = ball_customize;
888             
889             setorigin(self.ballcarried, self.origin + self.view_ofs + 
890                       v_forward * autocvar_g_nexball_viewmodel_offset_x + 
891                       v_right * autocvar_g_nexball_viewmodel_offset_y + 
892                       v_up * autocvar_g_nexball_viewmodel_offset_z);    
893                       
894             // 'safe passing'
895             if(autocvar_g_nexball_safepass_maxdist)
896             {
897                 if(self.ballcarried.wait < time && self.ballcarried.enemy)
898                 {
899                     //centerprint(self, sprintf("Lost lock on %s", self.ballcarried.enemy.netname));
900                     self.ballcarried.enemy = world;
901                 }
902                     
903                 
904                 //tracebox(self.origin + self.view_ofs, '-2 -2 -2', '2 2 2', self.origin + self.view_ofs + v_forward * autocvar_g_nexball_safepass_maxdist);
905                 crosshair_trace(self);
906                 if( trace_ent && 
907                     trace_ent.flags & FL_CLIENT &&
908                     trace_ent.deadflag == DEAD_NO &&
909                     trace_ent.team == self.team &&
910                     vlen(trace_ent.origin - self.origin) <= autocvar_g_nexball_safepass_maxdist )
911                 {
912                     
913                     //if(self.ballcarried.enemy != trace_ent)
914                     //    centerprint(self, sprintf("Locked to %s", trace_ent.netname));
915                     self.ballcarried.enemy = trace_ent;
916                     self.ballcarried.wait = time + autocvar_g_nexball_safepass_holdtime;
917                     
918                     
919                 }
920             }
921         }
922         else
923         {            
924             if(self.weaponentity.weapons)
925             {
926                 self.weapons = self.weaponentity.weapons;        
927                 weapon_action(WEP_PORTO, WR_RESETPLAYER);
928                 self.switchweapon = self.weaponentity.switchweapon;
929                 W_SwitchWeapon(self.switchweapon);
930                 
931                 self.weaponentity.weapons = 0;
932             }
933         }
934         
935     }
936     return FALSE;
937 }
938
939 MUTATOR_HOOKFUNCTION(nexball_PlayerSpawn)
940 {    
941     self.weaponentity.weapons = 0;
942     
943     if(nexball_mode & NBM_BASKETBALL)
944         self.weapons |= W_WeaponBit(WEP_PORTO);
945     else
946         self.weapons = 0; //    W_WeaponBit(WEP_PORTO);
947
948     return FALSE;
949 }
950
951 MUTATOR_DEFINITION(gamemode_nexball)
952 {
953         MUTATOR_HOOK(PlayerDies, nexball_BallDrop, CBC_ORDER_ANY);
954         MUTATOR_HOOK(MakePlayerObserver, nexball_BallDrop, CBC_ORDER_ANY);
955         MUTATOR_HOOK(ClientDisconnect, nexball_BallDrop, CBC_ORDER_ANY);
956         MUTATOR_HOOK(BuildMutatorsPrettyString, nexball_BuildMutatorsPrettyString, CBC_ORDER_ANY);
957         MUTATOR_HOOK(BuildMutatorsString, nexball_BuildMutatorsString, CBC_ORDER_ANY);
958         MUTATOR_HOOK(PlayerSpawn, nexball_PlayerSpawn, CBC_ORDER_ANY);
959         MUTATOR_HOOK(PlayerPreThink, nexball_PlayerPreThink, CBC_ORDER_ANY);
960
961         MUTATOR_ONADD
962         {
963             g_nexball = 1;
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         return 0;
983 }