]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/nexball/sv_nexball.qc
Further cleanup of defs.qh
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / nexball / sv_nexball.qc
1 #include "sv_nexball.qh"
2
3 #include <server/client.qh>
4 #include <server/command/vote.qh>
5 #include <server/gamelog.qh>
6 #include <common/ent_cs.qh>
7 #include <common/mapobjects/triggers.qh>
8
9 .entity ballcarried;
10
11 int autocvar_g_nexball_goalleadlimit;
12 #define autocvar_g_nexball_goallimit cvar("g_nexball_goallimit")
13
14 bool autocvar_g_nexball_basketball_jumppad = true;
15 float autocvar_g_nexball_basketball_bouncefactor;
16 float autocvar_g_nexball_basketball_bouncestop;
17 float autocvar_g_nexball_basketball_carrier_highspeed;
18 bool autocvar_g_nexball_basketball_meter;
19 float autocvar_g_nexball_basketball_meter_maxpower;
20 float autocvar_g_nexball_basketball_meter_minpower;
21 float autocvar_g_nexball_delay_collect;
22 float autocvar_g_nexball_delay_goal;
23 float autocvar_g_nexball_delay_start;
24 bool autocvar_g_nexball_football_jumppad = true;
25 float autocvar_g_nexball_football_bouncefactor;
26 float autocvar_g_nexball_football_bouncestop;
27 bool autocvar_g_nexball_radar_showallplayers;
28 bool autocvar_g_nexball_sound_bounce;
29 int autocvar_g_nexball_trail_color;
30 bool autocvar_g_nexball_playerclip_collisions = true;
31
32 float autocvar_g_nexball_safepass_turnrate;
33 float autocvar_g_nexball_safepass_maxdist;
34 float autocvar_g_nexball_safepass_holdtime;
35 float autocvar_g_nexball_viewmodel_scale;
36 float autocvar_g_nexball_tackling;
37 vector autocvar_g_nexball_viewmodel_offset;
38
39 float autocvar_g_balance_nexball_primary_animtime;
40 float autocvar_g_balance_nexball_primary_refire;
41 float autocvar_g_balance_nexball_primary_speed;
42 float autocvar_g_balance_nexball_secondary_animtime;
43 float autocvar_g_balance_nexball_secondary_force;
44 float autocvar_g_balance_nexball_secondary_lifetime;
45 float autocvar_g_balance_nexball_secondary_refire;
46 float autocvar_g_balance_nexball_secondary_speed;
47
48 void basketball_touch(entity this, entity toucher);
49 void football_touch(entity this, entity toucher);
50 void ResetBall(entity this);
51 const int NBM_NONE = 0;
52 const int NBM_FOOTBALL = 2;
53 const int NBM_BASKETBALL = 4;
54 float nexball_mode;
55
56 float OtherTeam(float t)  //works only if there are two teams on the map!
57 {
58         entity e;
59         e = find(NULL, classname, "nexball_team");
60         if(e.team == t)
61                 e = find(e, classname, "nexball_team");
62         return e.team;
63 }
64
65 const int ST_NEXBALL_GOALS = 1;
66 void nb_ScoreRules(int teams)
67 {
68     GameRules_scoring(teams, 0, 0, {
69         field_team(ST_NEXBALL_GOALS, "goals", SFL_SORT_PRIO_PRIMARY);
70         field(SP_NEXBALL_GOALS, "goals", SFL_SORT_PRIO_PRIMARY);
71         field(SP_NEXBALL_FAULTS, "faults", SFL_SORT_PRIO_SECONDARY | SFL_LOWER_IS_BETTER);
72     });
73 }
74
75 void LogNB(string mode, entity actor)
76 {
77         string s;
78         if(!autocvar_sv_eventlog)
79                 return;
80         s = strcat(":nexball:", mode);
81         if(actor != NULL)
82                 s = strcat(s, ":", ftos(actor.playerid));
83         GameLogEcho(s);
84 }
85
86 void ball_restart(entity this)
87 {
88         if(this.owner)
89                 DropBall(this, this.owner.origin, '0 0 0');
90         ResetBall(this);
91 }
92
93 void nexball_setstatus(entity this)
94 {
95         this.items &= ~IT_KEY1;
96         if(this.ballcarried)
97         {
98                 if(this.ballcarried.teamtime && (this.ballcarried.teamtime < time))
99                 {
100                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_NEXBALL_RETURN_HELD));
101                         entity e = this.ballcarried;
102                         DropBall(this.ballcarried, this.ballcarried.owner.origin, '0 0 0');
103                         ResetBall(e);
104                 }
105                 else
106                         this.items |= IT_KEY1;
107         }
108 }
109
110 void relocate_nexball(entity this)
111 {
112         tracebox(this.origin, BALL_MINS, BALL_MAXS, this.origin, true, this);
113         if(trace_startsolid)
114         {
115                 vector o = this.origin;
116                 if (!move_out_of_solid(this)) {
117                         objerror(this, "could not get out of solid at all!");
118         }
119         LOG_INFOF(
120             "^1NOTE: this map needs FIXING. %s at %s needs to be moved out of solid, e.g. by %s",
121             this.classname,
122             vtos(o - '0 0 1'),
123             vtos(this.origin - o)
124         );
125                 this.origin = o;
126         }
127 }
128
129 void DropOwner(entity this)
130 {
131         entity ownr;
132         ownr = this.owner;
133         DropBall(this, ownr.origin, ownr.velocity);
134         makevectors(ownr.v_angle.y * '0 1 0');
135         ownr.velocity += ('0 0 0.75' - v_forward) * 1000;
136         UNSET_ONGROUND(ownr);
137 }
138
139 void GiveBall(entity plyr, entity ball)
140 {
141         .entity weaponentity = weaponentities[0]; // TODO: find ballstealer
142         entity ownr = ball.owner;
143         if(ownr)
144         {
145                 ownr.effects &= ~autocvar_g_nexball_basketball_effects_default;
146                 ownr.ballcarried = NULL;
147                 GameRules_scoring_vip(ownr, false);
148                 if(STAT(NB_METERSTART, ownr))
149                 {
150                         STAT(NB_METERSTART, ownr) = 0;
151                         ownr.(weaponentity).state = WS_READY;
152                 }
153                 WaypointSprite_Kill(ownr.waypointsprite_attachedforcarrier);
154         }
155         else
156         {
157                 WaypointSprite_Kill(ball.waypointsprite_attachedforcarrier);
158         }
159
160         //setattachment(ball, plyr, "");
161         setorigin(ball, plyr.origin + plyr.view_ofs);
162
163         if(ball.team != plyr.team)
164                 ball.teamtime = time + autocvar_g_nexball_basketball_delay_hold_forteam;
165
166         ball.owner = ball.pusher = plyr; //"owner" is set to the player carrying, "pusher" to the last player who touched it
167         ball.weaponentity_fld = weaponentity;
168         ball.team = plyr.team;
169         plyr.ballcarried = ball;
170         GameRules_scoring_vip(plyr, true);
171         ball.nb_dropper = plyr;
172
173         plyr.effects |= autocvar_g_nexball_basketball_effects_default;
174         ball.effects &= ~autocvar_g_nexball_basketball_effects_default;
175
176         ball.velocity = '0 0 0';
177         set_movetype(ball, MOVETYPE_NONE);
178         settouch(ball, func_null);
179         ball.effects |= EF_NOSHADOW;
180         ball.scale = 1; // scale down.
181
182         WaypointSprite_AttachCarrier(WP_NbBall, plyr, RADARICON_FLAGCARRIER);
183         WaypointSprite_UpdateRule(plyr.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
184
185         if(autocvar_g_nexball_basketball_delay_hold)
186         {
187                 setthink(ball, DropOwner);
188                 ball.nextthink = time + autocvar_g_nexball_basketball_delay_hold;
189         }
190
191         STAT(WEAPONS, plyr.(weaponentity)) = STAT(WEAPONS, plyr);
192         plyr.m_switchweapon = plyr.(weaponentity).m_weapon;
193         STAT(WEAPONS, plyr) = WEPSET(NEXBALL);
194         Weapon w = WEP_NEXBALL;
195         w.wr_resetplayer(w, plyr);
196         plyr.(weaponentity).m_switchweapon = WEP_NEXBALL;
197         W_SwitchWeapon(plyr, WEP_NEXBALL, weaponentity);
198 }
199
200 void DropBall(entity ball, vector org, vector vel)
201 {
202         ball.effects |= autocvar_g_nexball_basketball_effects_default;
203         ball.effects &= ~EF_NOSHADOW;
204         ball.owner.effects &= ~autocvar_g_nexball_basketball_effects_default;
205
206         setattachment(ball, NULL, "");
207         setorigin(ball, org);
208         set_movetype(ball, MOVETYPE_BOUNCE);
209         UNSET_ONGROUND(ball);
210         ball.scale = ball_scale;
211         ball.velocity = vel;
212         ball.nb_droptime = time;
213         settouch(ball, basketball_touch);
214         setthink(ball, ResetBall);
215         ball.nextthink = min(time + autocvar_g_nexball_delay_idle, ball.teamtime);
216
217         if(STAT(NB_METERSTART, ball.owner))
218         {
219                 STAT(NB_METERSTART, ball.owner) = 0;
220                 .entity weaponentity = ball.weaponentity_fld;
221                 ball.owner.(weaponentity).state = WS_READY;
222         }
223
224         WaypointSprite_Kill(ball.owner.waypointsprite_attachedforcarrier);
225         WaypointSprite_Spawn(WP_NbBall, 0, 0, ball, '0 0 64', NULL, ball.team, ball, waypointsprite_attachedforcarrier, false, RADARICON_FLAGCARRIER); // no health bar please
226         WaypointSprite_UpdateRule(ball.waypointsprite_attachedforcarrier, 0, SPRITERULE_DEFAULT);
227
228         entity e = ball.owner; ball.owner = NULL;
229         e.ballcarried = NULL;
230         GameRules_scoring_vip(e, false);
231 }
232
233 void InitBall(entity this)
234 {
235         if(game_stopped) return;
236         UNSET_ONGROUND(this);
237         set_movetype(this, MOVETYPE_BOUNCE);
238         if(this.classname == "nexball_basketball")
239                 settouch(this, basketball_touch);
240         else if(this.classname == "nexball_football")
241                 settouch(this, football_touch);
242         this.cnt = 0;
243         setthink(this, ResetBall);
244         this.nextthink = time + autocvar_g_nexball_delay_idle + 3;
245         this.teamtime = 0;
246         this.pusher = NULL;
247         this.team = false;
248         _sound(this, CH_TRIGGER, this.noise1, VOL_BASE, ATTEN_NORM);
249         WaypointSprite_Ping(this.waypointsprite_attachedforcarrier);
250         LogNB("init", NULL);
251 }
252
253 void ResetBall(entity this)
254 {
255         if(this.cnt < 2)        // step 1
256         {
257                 if(time == this.teamtime)
258                         Send_Notification(NOTIF_ALL, NULL, MSG_INFO, APP_TEAM_NUM(this.team, INFO_NEXBALL_RETURN_HELD));
259
260                 settouch(this, func_null);
261                 set_movetype(this, MOVETYPE_NOCLIP);
262                 this.velocity = '0 0 0'; // just in case?
263                 if(!this.cnt)
264                         LogNB("resetidle", NULL);
265                 this.cnt = 2;
266                 this.nextthink = time;
267         }
268         else if(this.cnt < 4)     // step 2 and 3
269         {
270 //              dprint("Step ", ftos(this.cnt), ": Calculated velocity: ", vtos(this.spawnorigin - this.origin), ", time: ", ftos(time), "\n");
271                 this.velocity = (this.spawnorigin - this.origin) * (this.cnt - 1); // 1 or 0.5 second movement
272                 this.nextthink = time + 0.5;
273                 this.cnt += 1;
274         }
275         else     // step 4
276         {
277 //              dprint("Step 4: time: ", ftos(time), "\n");
278                 if(vdist(this.origin - this.spawnorigin, >, 10)) // should not happen anymore
279                         LOG_TRACE("The ball moved too far away from its spawn origin.\nOffset: ",
280                                    vtos(this.origin - this.spawnorigin), " Velocity: ", vtos(this.velocity), "\n");
281                 this.velocity = '0 0 0';
282                 setorigin(this, this.spawnorigin); // make sure it's positioned correctly anyway
283                 set_movetype(this, MOVETYPE_NONE);
284                 setthink(this, InitBall);
285                 this.nextthink = max(time, game_starttime) + autocvar_g_nexball_delay_start;
286         }
287 }
288
289 void football_touch(entity this, entity toucher)
290 {
291         if(toucher.solid == SOLID_BSP)
292         {
293                 if(time > this.lastground + 0.1)
294                 {
295                         _sound(this, CH_TRIGGER, this.noise, VOL_BASE, ATTEN_NORM);
296                         this.lastground = time;
297                 }
298                 if(this.velocity && !this.cnt)
299                         this.nextthink = time + autocvar_g_nexball_delay_idle;
300                 return;
301         }
302         if (!IS_PLAYER(toucher) && !IS_VEHICLE(toucher))
303                 return;
304         if(GetResource(toucher, RES_HEALTH) < 1)
305                 return;
306         if(!this.cnt)
307                 this.nextthink = time + autocvar_g_nexball_delay_idle;
308
309         this.pusher = toucher;
310         this.team = toucher.team;
311
312         if(autocvar_g_nexball_football_physics == -1)   // MrBougo try 1, before decompiling Rev's original
313         {
314                 if(toucher.velocity)
315                         this.velocity = toucher.velocity * 1.5 + '0 0 1' * autocvar_g_nexball_football_boost_up;
316         }
317         else if(autocvar_g_nexball_football_physics == 1)         // MrBougo's modded Rev style: partially independant of the height of the aiming point
318         {
319                 makevectors(toucher.v_angle);
320                 this.velocity = toucher.velocity + v_forward * autocvar_g_nexball_football_boost_forward + '0 0 1' * autocvar_g_nexball_football_boost_up;
321         }
322         else if(autocvar_g_nexball_football_physics == 2)         // 2nd mod try: totally independant. Really playable!
323         {
324                 makevectors(toucher.v_angle.y * '0 1 0');
325                 this.velocity = toucher.velocity + v_forward * autocvar_g_nexball_football_boost_forward + v_up * autocvar_g_nexball_football_boost_up;
326         }
327         else     // Revenant's original style (from the original mod's disassembly, acknowledged by Revenant)
328         {
329                 makevectors(toucher.v_angle);
330                 this.velocity = toucher.velocity + v_forward * autocvar_g_nexball_football_boost_forward + v_up * autocvar_g_nexball_football_boost_up;
331         }
332         this.avelocity = -250 * v_forward;  // maybe there is a way to make it look better?
333 }
334
335 void basketball_touch(entity this, entity toucher)
336 {
337         if(toucher.ballcarried)
338         {
339                 football_touch(this, toucher);
340                 return;
341         }
342         if(!this.cnt && IS_PLAYER(toucher) && !STAT(FROZEN, toucher) && !IS_DEAD(toucher) && (toucher != this.nb_dropper || time > this.nb_droptime + autocvar_g_nexball_delay_collect))
343         {
344                 if(GetResource(toucher, RES_HEALTH) < 1)
345                         return;
346                 LogNB("caught", toucher);
347                 GiveBall(toucher, this);
348         }
349         else if(toucher.solid == SOLID_BSP)
350         {
351                 _sound(this, CH_TRIGGER, this.noise, VOL_BASE, ATTEN_NORM);
352                 if(this.velocity && !this.cnt)
353                         this.nextthink = min(time + autocvar_g_nexball_delay_idle, this.teamtime);
354         }
355 }
356
357 void GoalTouch(entity this, entity toucher)
358 {
359         entity ball;
360         float isclient, pscore, otherteam;
361         string pname;
362
363         if(game_stopped) return;
364         if((this.spawnflags & GOAL_TOUCHPLAYER) && toucher.ballcarried)
365                 ball = toucher.ballcarried;
366         else
367                 ball = toucher;
368         if(ball.classname != "nexball_basketball")
369                 if(ball.classname != "nexball_football")
370                         return;
371         if((!ball.pusher && this.team != GOAL_OUT) || ball.cnt)
372                 return;
373         EXACTTRIGGER_TOUCH(this, toucher);
374
375
376         if(NumTeams(nb_teams) == 2)
377                 otherteam = OtherTeam(ball.team);
378         else
379                 otherteam = 0;
380
381         if((isclient = IS_CLIENT(ball.pusher)))
382                 pname = ball.pusher.netname;
383         else
384                 pname = "Someone (?)";
385
386         if(ball.team == this.team)               //owngoal (regular goals)
387         {
388                 LogNB("owngoal", ball.pusher);
389                 bprint("Boo! ", pname, "^7 scored a goal against their own team!\n");
390                 pscore = -1;
391         }
392         else if(this.team == GOAL_FAULT)
393         {
394                 LogNB("fault", ball.pusher);
395                 if(NumTeams(nb_teams) == 2)
396                         bprint(Team_ColoredFullName(otherteam), " gets a point due to ", pname, "^7's silliness.\n");
397                 else
398                         bprint(Team_ColoredFullName(ball.team), " loses a point due to ", pname, "^7's silliness.\n");
399                 pscore = -1;
400         }
401         else if(this.team == GOAL_OUT)
402         {
403                 LogNB("out", ball.pusher);
404                 if((this.spawnflags & GOAL_TOUCHPLAYER) && ball.owner)
405                         bprint(pname, "^7 went out of bounds.\n");
406                 else
407                         bprint("The ball was returned.\n");
408                 pscore = 0;
409         }
410         else                                                       //score
411         {
412                 LogNB(strcat("goal:", ftos(this.team)), ball.pusher);
413                 bprint("Goaaaaal! ", pname, "^7 scored a point for the ", Team_ColoredFullName(ball.team), ".\n");
414                 pscore = 1;
415         }
416
417         _sound(ball, CH_TRIGGER, this.noise, VOL_BASE, ATTEN_NONE);
418
419         if(ball.team && pscore)
420         {
421                 if(NumTeams(nb_teams) == 2 && pscore < 0)
422                         TeamScore_AddToTeam(otherteam, ST_NEXBALL_GOALS, -pscore);
423                 else
424                         TeamScore_AddToTeam(ball.team, ST_NEXBALL_GOALS, pscore);
425         }
426         if(isclient)
427         {
428                 if(pscore > 0)
429                         GameRules_scoring_add(ball.pusher, NEXBALL_GOALS, pscore);
430                 else if(pscore < 0)
431                         GameRules_scoring_add(ball.pusher, NEXBALL_FAULTS, -pscore);
432         }
433
434         if(ball.owner)  // Happens on spawnflag GOAL_TOUCHPLAYER
435                 DropBall(ball, ball.owner.origin, ball.owner.velocity);
436
437         WaypointSprite_Ping(ball.waypointsprite_attachedforcarrier);
438
439         ball.cnt = 1;
440         setthink(ball, ResetBall);
441         if(ball.classname == "nexball_basketball")
442                 settouch(ball, football_touch); // better than func_null: football control until the ball gets reset
443         ball.nextthink = time + autocvar_g_nexball_delay_goal * (this.team != GOAL_OUT);
444 }
445
446 //=======================//
447 //         team ents       //
448 //=======================//
449 spawnfunc(nexball_team)
450 {
451         if(!g_nexball)
452         {
453                 delete(this);
454                 return;
455         }
456         this.team = this.cnt + 1;
457 }
458
459 void nb_spawnteam(string teamname, float teamcolor)
460 {
461         LOG_TRACE("^2spawned team ", teamname);
462         entity e = new(nexball_team);
463         e.netname = teamname;
464         e.cnt = teamcolor;
465         e.team = e.cnt + 1;
466         //nb_teams += 1;
467 }
468
469 void nb_spawnteams()
470 {
471         bool t_red = false, t_blue = false, t_yellow = false, t_pink = false;
472         entity e;
473         for(e = NULL; (e = find(e, classname, "nexball_goal"));)
474         {
475                 switch(e.team)
476                 {
477                 case NUM_TEAM_1:
478                         if(!t_red)
479                         {
480                                 nb_spawnteam("Red", e.team-1)   ;
481                                 nb_teams |= BIT(0);
482                                 t_red = true;
483                         }
484                         break;
485                 case NUM_TEAM_2:
486                         if(!t_blue)
487                         {
488                                 nb_spawnteam("Blue", e.team-1)  ;
489                                 t_blue = true;
490                                 nb_teams |= BIT(1);
491                         }
492                         break;
493                 case NUM_TEAM_3:
494                         if(!t_yellow)
495                         {
496                                 nb_spawnteam("Yellow", e.team-1);
497                                 t_yellow = true;
498                                 nb_teams |= BIT(2);
499                         }
500                         break;
501                 case NUM_TEAM_4:
502                         if(!t_pink)
503                         {
504                                 nb_spawnteam("Pink", e.team-1)  ;
505                                 t_pink = true;
506                                 nb_teams |= BIT(3);
507                         }
508                         break;
509                 }
510         }
511 }
512
513 void nb_delayedinit(entity this)
514 {
515         if(find(NULL, classname, "nexball_team") == NULL)
516                 nb_spawnteams();
517         nb_ScoreRules(nb_teams);
518 }
519
520
521 //=======================//
522 //        spawnfuncs       //
523 //=======================//
524
525 void SpawnBall(entity this)
526 {
527         if(!g_nexball) { delete(this); return; }
528
529 //      balls += 4; // using the remaining bits to count balls will leave more than the max edict count, so it's fine
530
531         if(this.model == "")
532         {
533                 this.model = "models/nexball/ball.md3";
534                 this.scale = 1.3;
535         }
536
537         precache_model(this.model);
538         _setmodel(this, this.model);
539         setsize(this, BALL_MINS, BALL_MAXS);
540         ball_scale = this.scale;
541
542         relocate_nexball(this);
543         this.spawnorigin = this.origin;
544
545         this.effects = this.effects | EF_LOWPRECISION;
546
547         if(cvar(strcat("g_", this.classname, "_trail")))  //nexball_basketball :p
548         {
549                 this.glow_color = autocvar_g_nexball_trail_color;
550                 this.glow_trail = true;
551         }
552
553         set_movetype(this, MOVETYPE_FLY);
554
555         if(autocvar_g_nexball_playerclip_collisions)
556                 this.dphitcontentsmask = DPCONTENTS_BODY | DPCONTENTS_SOLID | DPCONTENTS_PLAYERCLIP;
557
558         if(!autocvar_g_nexball_sound_bounce)
559                 this.noise = "";
560         else if(this.noise == "")
561                 this.noise = strzone(SND(NB_BOUNCE));
562         //bounce sound placeholder (FIXME)
563         if(this.noise1 == "")
564                 this.noise1 = strzone(SND(NB_DROP));
565         //ball drop sound placeholder (FIXME)
566         if(this.noise2 == "")
567                 this.noise2 = strzone(SND(NB_STEAL));
568         //stealing sound placeholder (FIXME)
569         if(this.noise) precache_sound(this.noise);
570         precache_sound(this.noise1);
571         precache_sound(this.noise2);
572
573         WaypointSprite_AttachCarrier(WP_NbBall, this, RADARICON_FLAGCARRIER); // the ball's team is not set yet, no rule update needed
574
575         this.reset = ball_restart;
576         setthink(this, InitBall);
577         this.nextthink = game_starttime + autocvar_g_nexball_delay_start;
578 }
579
580 spawnfunc(nexball_basketball)
581 {
582         nexball_mode |= NBM_BASKETBALL;
583         this.classname = "nexball_basketball";
584         if (!(balls & BALL_BASKET))
585         {
586                 /*
587                 CVTOV(g_nexball_basketball_effects_default);
588                 CVTOV(g_nexball_basketball_delay_hold);
589                 CVTOV(g_nexball_basketball_delay_hold_forteam);
590                 CVTOV(g_nexball_basketball_teamsteal);
591                 */
592                 autocvar_g_nexball_basketball_effects_default = autocvar_g_nexball_basketball_effects_default & BALL_EFFECTMASK;
593         }
594         if(!this.effects)
595                 this.effects = autocvar_g_nexball_basketball_effects_default;
596         this.solid = SOLID_TRIGGER;
597         this.pushable = autocvar_g_nexball_basketball_jumppad;
598         balls |= BALL_BASKET;
599         this.bouncefactor = autocvar_g_nexball_basketball_bouncefactor;
600         this.bouncestop = autocvar_g_nexball_basketball_bouncestop;
601         SpawnBall(this);
602 }
603
604 spawnfunc(nexball_football)
605 {
606         nexball_mode |= NBM_FOOTBALL;
607         this.classname = "nexball_football";
608         this.solid = SOLID_TRIGGER;
609         balls |= BALL_FOOT;
610         this.pushable = autocvar_g_nexball_football_jumppad;
611         this.bouncefactor = autocvar_g_nexball_football_bouncefactor;
612         this.bouncestop = autocvar_g_nexball_football_bouncestop;
613         SpawnBall(this);
614 }
615
616 bool nb_Goal_Customize(entity this, entity client)
617 {
618         entity e = WaypointSprite_getviewentity(client);
619         entity wp_owner = this.owner;
620         if(SAME_TEAM(e, wp_owner)) { return false; }
621
622         return true;
623 }
624
625 void SpawnGoal(entity this)
626 {
627         if(!g_nexball) { delete(this); return; }
628
629         EXACTTRIGGER_INIT;
630
631         if(this.team != GOAL_OUT && Team_IsValidTeam(this.team))
632         {
633                 entity wp = WaypointSprite_SpawnFixed(WP_NbGoal, (this.absmin + this.absmax) * 0.5, this, sprite, RADARICON_NONE);
634                 wp.colormod = ((this.team) ? Team_ColorRGB(this.team) : '1 0.5 0');
635                 setcefc(this.sprite, nb_Goal_Customize);
636         }
637
638         this.classname = "nexball_goal";
639         if(this.noise == "")
640                 this.noise = "ctf/respawn.wav";
641         precache_sound(this.noise);
642         settouch(this, GoalTouch);
643 }
644
645 spawnfunc(nexball_redgoal)
646 {
647         this.team = NUM_TEAM_1;
648         SpawnGoal(this);
649 }
650 spawnfunc(nexball_bluegoal)
651 {
652         this.team = NUM_TEAM_2;
653         SpawnGoal(this);
654 }
655 spawnfunc(nexball_yellowgoal)
656 {
657         this.team = NUM_TEAM_3;
658         SpawnGoal(this);
659 }
660 spawnfunc(nexball_pinkgoal)
661 {
662         this.team = NUM_TEAM_4;
663         SpawnGoal(this);
664 }
665
666 spawnfunc(nexball_fault)
667 {
668         this.team = GOAL_FAULT;
669         if(this.noise == "")
670                 this.noise = strzone(SND(TYPEHIT));
671         SpawnGoal(this);
672 }
673
674 spawnfunc(nexball_out)
675 {
676         this.team = GOAL_OUT;
677         if(this.noise == "")
678                 this.noise = strzone(SND(TYPEHIT));
679         SpawnGoal(this);
680 }
681
682 //
683 //Spawnfuncs preserved for compatibility
684 //
685
686 spawnfunc(ball)
687 {
688         spawnfunc_nexball_football(this);
689 }
690 spawnfunc(ball_football)
691 {
692         spawnfunc_nexball_football(this);
693 }
694 spawnfunc(ball_basketball)
695 {
696         spawnfunc_nexball_basketball(this);
697 }
698 // The "red goal" is defended by blue team. A ball in there counts as a point for red.
699 spawnfunc(ball_redgoal)
700 {
701         spawnfunc_nexball_bluegoal(this);       // I blame Revenant
702 }
703 spawnfunc(ball_bluegoal)
704 {
705         spawnfunc_nexball_redgoal(this);        // but he didn't mean to cause trouble :p
706 }
707 spawnfunc(ball_fault)
708 {
709         spawnfunc_nexball_fault(this);
710 }
711 spawnfunc(ball_bound)
712 {
713         spawnfunc_nexball_out(this);
714 }
715
716 bool ball_customize(entity this, entity client)
717 {
718         if(!this.owner)
719         {
720                 this.effects &= ~EF_FLAME;
721                 this.scale = 1;
722                 setcefc(this, func_null);
723                 return true;
724         }
725
726         if(client == this.owner)
727         {
728                 this.scale = autocvar_g_nexball_viewmodel_scale;
729                 if(this.enemy)
730                         this.effects |= EF_FLAME;
731                 else
732                         this.effects &= ~EF_FLAME;
733         }
734         else
735         {
736                 this.effects &= ~EF_FLAME;
737                 this.scale = 1;
738         }
739
740         return true;
741 }
742
743 void nb_DropBall(entity player)
744 {
745         if(player.ballcarried && g_nexball)
746                 DropBall(player.ballcarried, player.origin, player.velocity);
747 }
748
749 MUTATOR_HOOKFUNCTION(nb, ClientDisconnect)
750 {
751         entity player = M_ARGV(0, entity);
752
753         nb_DropBall(player);
754 }
755
756 MUTATOR_HOOKFUNCTION(nb, PlayerDies)
757 {
758         entity frag_target = M_ARGV(2, entity);
759
760         nb_DropBall(frag_target);
761 }
762
763 MUTATOR_HOOKFUNCTION(nb, MakePlayerObserver)
764 {
765         entity player = M_ARGV(0, entity);
766
767         nb_DropBall(player);
768         return false;
769 }
770
771 MUTATOR_HOOKFUNCTION(nb, PlayerPreThink)
772 {
773         entity player = M_ARGV(0, entity);
774
775         makevectors(player.v_angle);
776         if(nexball_mode & NBM_BASKETBALL)
777         {
778                 if(player.ballcarried)
779                 {
780                         // 'view ball'
781                         player.ballcarried.velocity = player.velocity;
782                         setcefc(player.ballcarried, ball_customize);
783
784                         vector org = player.origin + player.view_ofs +
785                                           v_forward * autocvar_g_nexball_viewmodel_offset.x +
786                                           v_right * autocvar_g_nexball_viewmodel_offset.y +
787                                           v_up * autocvar_g_nexball_viewmodel_offset.z;
788                         setorigin(player.ballcarried, org);
789
790                         // 'safe passing'
791                         if(autocvar_g_nexball_safepass_maxdist)
792                         {
793                                 if(player.ballcarried.wait < time && player.ballcarried.enemy)
794                                 {
795                                         //centerprint(player, sprintf("Lost lock on %s", player.ballcarried.enemy.netname));
796                                         player.ballcarried.enemy = NULL;
797                                 }
798
799
800                                 //tracebox(player.origin + player.view_ofs, '-2 -2 -2', '2 2 2', player.origin + player.view_ofs + v_forward * autocvar_g_nexball_safepass_maxdist);
801                                 crosshair_trace(player);
802                                 if( trace_ent &&
803                                         IS_CLIENT(trace_ent) &&
804                                         !IS_DEAD(trace_ent) &&
805                                         trace_ent.team == player.team &&
806                                         vdist(trace_ent.origin - player.origin, <=, autocvar_g_nexball_safepass_maxdist) )
807                                 {
808
809                                         //if(player.ballcarried.enemy != trace_ent)
810                                         //      centerprint(player, sprintf("Locked to %s", trace_ent.netname));
811                                         player.ballcarried.enemy = trace_ent;
812                                         player.ballcarried.wait = time + autocvar_g_nexball_safepass_holdtime;
813
814
815                                 }
816                         }
817                 }
818                 else
819                 {
820                         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
821                         {
822                                 .entity weaponentity = weaponentities[slot];
823
824                                 if(STAT(WEAPONS, player.(weaponentity)))
825                                 {
826                                         STAT(WEAPONS, player) = STAT(WEAPONS, player.(weaponentity));
827                                         Weapon w = WEP_NEXBALL;
828                                         w.wr_resetplayer(w, player);
829                                         player.(weaponentity).m_switchweapon = player.m_switchweapon;
830                                         W_SwitchWeapon(player, player.(weaponentity).m_switchweapon, weaponentity);
831
832                                         STAT(WEAPONS, player.(weaponentity)) = '0 0 0';
833                                 }
834                         }
835                 }
836
837         }
838
839         nexball_setstatus(player);
840 }
841
842 MUTATOR_HOOKFUNCTION(nb, SpectateCopy)
843 {
844         entity spectatee = M_ARGV(0, entity);
845         entity client = M_ARGV(1, entity);
846
847         STAT(NB_METERSTART, client) = STAT(NB_METERSTART, spectatee);
848 }
849
850 MUTATOR_HOOKFUNCTION(nb, PlayerSpawn)
851 {
852         entity player = M_ARGV(0, entity);
853
854         STAT(NB_METERSTART, player) = 0;
855         for(int slot = 0; slot < MAX_WEAPONSLOTS; ++slot)
856         {
857                 .entity weaponentity = weaponentities[slot];
858                 STAT(WEAPONS, player.(weaponentity)) = '0 0 0';
859         }
860
861         if (nexball_mode & NBM_BASKETBALL)
862                 STAT(WEAPONS, player) |= WEPSET(NEXBALL);
863         else
864                 STAT(WEAPONS, player) = '0 0 0';
865
866         return false;
867 }
868
869 MUTATOR_HOOKFUNCTION(nb, PlayerPhysics_UpdateStats)
870 {
871         entity player = M_ARGV(0, entity);
872         // these automatically reset, no need to worry
873
874         if(player.ballcarried)
875                 STAT(MOVEVARS_HIGHSPEED, player) *= autocvar_g_nexball_basketball_carrier_highspeed;
876 }
877
878 MUTATOR_HOOKFUNCTION(nb, ForbidThrowCurrentWeapon)
879 {
880         //entity player = M_ARGV(0, entity);
881         entity wepent = M_ARGV(1, entity);
882
883         return wepent.m_weapon == WEP_NEXBALL;
884 }
885
886 MUTATOR_HOOKFUNCTION(nb, ForbidDropCurrentWeapon)
887 {
888         //entity player = M_ARGV(0, entity);
889         int wep = M_ARGV(1, int);
890
891         return wep == WEP_MORTAR.m_id; // TODO: what is this for?
892 }
893
894 MUTATOR_HOOKFUNCTION(nb, FilterItem)
895 {
896         entity item = M_ARGV(0, entity);
897
898         if(Item_IsLoot(item))
899         if(item.weapon == WEP_NEXBALL.m_id)
900                 return true;
901
902         return false;
903 }
904
905 MUTATOR_HOOKFUNCTION(nb, ItemTouch)
906 {
907         entity item = M_ARGV(0, entity);
908         entity toucher = M_ARGV(1, entity);
909
910         if(item.weapon && toucher.ballcarried)
911                 return MUT_ITEMTOUCH_RETURN; // no new weapons for you, mister!
912
913         return MUT_ITEMTOUCH_CONTINUE;
914 }
915
916 MUTATOR_HOOKFUNCTION(nb, TeamBalance_CheckAllowedTeams)
917 {
918         M_ARGV(1, string) = "nexball_team";
919         return true;
920 }
921
922 MUTATOR_HOOKFUNCTION(nb, WantWeapon)
923 {
924         M_ARGV(1, float) = 0; // weapon is set a few lines later, apparently
925         return true;
926 }
927
928 MUTATOR_HOOKFUNCTION(nb, DropSpecialItems)
929 {
930         entity frag_target = M_ARGV(0, entity);
931
932         if(frag_target.ballcarried)
933                 DropBall(frag_target.ballcarried, frag_target.origin, frag_target.velocity);
934
935         return false;
936 }
937
938 MUTATOR_HOOKFUNCTION(nb, SendWaypoint)
939 {
940         M_ARGV(2, int) &= ~0x80;
941 }
942
943 REGISTER_MUTATOR(nb, false)
944 {
945     MUTATOR_STATIC();
946         MUTATOR_ONADD
947         {
948                 g_nexball_meter_period = autocvar_g_nexball_meter_period;
949                 if(g_nexball_meter_period <= 0)
950                         g_nexball_meter_period = 2; // avoid division by zero etc. due to silly users
951                 g_nexball_meter_period = rint(g_nexball_meter_period * 32) / 32; //Round to 1/32ths to send as a byte multiplied by 32
952
953                 // General settings
954                 /*
955                 CVTOV(g_nexball_football_boost_forward);   //100
956                 CVTOV(g_nexball_football_boost_up);             //200
957                 CVTOV(g_nexball_delay_idle);                       //10
958                 CVTOV(g_nexball_football_physics);               //0
959                 */
960                 radar_showenemies = autocvar_g_nexball_radar_showallplayers;
961
962                 InitializeEntity(NULL, nb_delayedinit, INITPRIO_GAMETYPE);
963                 WEP_NEXBALL.spawnflags &= ~WEP_FLAG_MUTATORBLOCKED;
964
965                 GameRules_teams(true);
966                 GameRules_limit_score(autocvar_g_nexball_goallimit);
967                 GameRules_limit_lead(autocvar_g_nexball_goalleadlimit);
968         }
969
970         MUTATOR_ONROLLBACK_OR_REMOVE
971         {
972                 WEP_NEXBALL.spawnflags |= WEP_FLAG_MUTATORBLOCKED;
973         }
974         return 0;
975 }