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