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