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