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