]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/gamemode_onslaught.qc
Merge branch 'master' into Mario/ons_updates
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / gamemode_onslaught.qc
1 #include "../controlpoint.qh"
2 #include "../generator.qh"
3
4 // =======================
5 // CaptureShield Functions
6 // =======================
7
8 bool ons_CaptureShield_Customize()
9 {
10         entity e = WaypointSprite_getviewentity(other);
11
12         if(!self.enemy.isshielded && (ons_ControlPoint_Attackable(self.enemy, e.team) > 0 || self.enemy.classname != "onslaught_controlpoint")) { return false; }
13         if(SAME_TEAM(self, e)) { return false; }
14
15         return true;
16 }
17
18 void ons_CaptureShield_Touch()
19 {
20         if(!self.enemy.isshielded && (ons_ControlPoint_Attackable(self.enemy, other.team) > 0 || self.enemy.classname != "onslaught_controlpoint")) { return; }
21         if(!IS_PLAYER(other)) { return; }
22         if(SAME_TEAM(other, self)) { return; }
23
24         vector mymid = (self.absmin + self.absmax) * 0.5;
25         vector othermid = (other.absmin + other.absmax) * 0.5;
26
27         Damage(other, self, self, 0, DEATH_HURTTRIGGER, mymid, normalize(othermid - mymid) * ons_captureshield_force);
28
29         if(IS_REAL_CLIENT(other))
30         {
31                 play2(other, "onslaught/damageblockedbyshield.wav");
32
33                 if(self.enemy.classname == "onslaught_generator")
34                         Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_ONS_GENERATOR_SHIELDED);
35                 else
36                         Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_ONS_CONTROLPOINT_SHIELDED);
37         }
38 }
39
40 void ons_CaptureShield_Reset()
41 {
42         self.colormap = self.enemy.colormap;
43         self.team = self.enemy.team;
44 }
45
46 void ons_CaptureShield_Spawn(entity generator, bool is_generator)
47 {
48         entity shield = spawn();
49
50         shield.enemy = generator;
51         shield.team = generator.team;
52         shield.colormap = generator.colormap;
53         shield.reset = ons_CaptureShield_Reset;
54         shield.touch = ons_CaptureShield_Touch;
55         shield.customizeentityforclient = ons_CaptureShield_Customize;
56         shield.classname = "ons_captureshield";
57         shield.effects = EF_ADDITIVE;
58         shield.movetype = MOVETYPE_NOCLIP;
59         shield.solid = SOLID_TRIGGER;
60         shield.avelocity = '7 0 11';
61         shield.scale = 1;
62         shield.model = ((is_generator) ? "models/onslaught/generator_shield.md3" : "models/onslaught/controlpoint_shield.md3");
63         
64         precache_model(shield.model);
65         setorigin(shield, generator.origin);
66         setmodel(shield, shield.model);
67         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
68 }
69
70
71 // ==========
72 // Junk Pile
73 // ==========
74
75 void ons_debug(string input)
76 {
77         switch(autocvar_g_onslaught_debug)
78         {
79                 case 1: dprint(input); break;
80                 case 2: print(input); break;
81         }
82 }
83
84 void FixSize(entity e)
85 {
86         e.mins_x = rint(e.mins_x);
87         e.mins_y = rint(e.mins_y);
88         e.mins_z = rint(e.mins_z);
89         
90         e.maxs_x = rint(e.maxs_x);
91         e.maxs_y = rint(e.maxs_y);
92         e.maxs_z = rint(e.maxs_z);
93 }
94
95 vector randompos(vector m1, vector m2)
96 {
97         vector v;
98         m2 = m2 - m1;
99         v_x = m2_x * random() + m1_x;
100         v_y = m2_y * random() + m1_y;
101         v_z = m2_z * random() + m1_z;
102         return  v;
103 }
104
105 void setmodel_fixsize(entity e, string m)
106 {
107         setmodel(e, m);
108         FixSize(e);
109 }
110
111 void onslaught_updatelinks()
112 {
113         entity l;
114         // first check if the game has ended
115         ons_debug("--- updatelinks ---\n");
116         // mark generators as being shielded and networked
117         for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
118         {
119                 if (l.iscaptured)
120                         ons_debug(strcat(etos(l), " (generator) belongs to team ", ftos(l.team), "\n"));
121                 else
122                         ons_debug(strcat(etos(l), " (generator) is destroyed\n"));
123                 l.islinked = l.iscaptured;
124                 l.isshielded = l.iscaptured;
125                 l.sprite.SendFlags |= 16;
126         }
127         // mark points as shielded and not networked
128         for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
129         {
130                 l.islinked = false;
131                 l.isshielded = true;
132                 int i;
133                 for(i = 0; i < 17; ++i) { l.isgenneighbor[i] = false; l.iscpneighbor[i] = false; }
134                 ons_debug(strcat(etos(l), " (point) belongs to team ", ftos(l.team), "\n"));
135                 l.sprite.SendFlags |= 16;
136         }
137         // flow power outward from the generators through the network
138         bool stop = false;
139         while (!stop)
140         {
141                 stop = true;
142                 for(l = ons_worldlinklist; l; l = l.ons_worldlinknext)
143                 {
144                         // if both points are captured by the same team, and only one of
145                         // them is powered, mark the other one as powered as well
146                         if (l.enemy.iscaptured && l.goalentity.iscaptured)
147                                 if (l.enemy.islinked != l.goalentity.islinked)
148                                         if(SAME_TEAM(l.enemy, l.goalentity))
149                                         {
150                                                 if (!l.goalentity.islinked)
151                                                 {
152                                                         stop = false;
153                                                         l.goalentity.islinked = true;
154                                                         ons_debug(strcat(etos(l), " (link) is marking ", etos(l.goalentity), " (point) because its team matches ", etos(l.enemy), " (point)\n"));
155                                                 }
156                                                 else if (!l.enemy.islinked)
157                                                 {
158                                                         stop = false;
159                                                         l.enemy.islinked = true;
160                                                         ons_debug(strcat(etos(l), " (link) is marking ", etos(l.enemy), " (point) because its team matches ", etos(l.goalentity), " (point)\n"));
161                                                 }
162                                         }
163                 }
164         }
165         // now that we know which points are powered we can mark their neighbors
166         // as unshielded if team differs
167         for(l = ons_worldlinklist; l; l = l.ons_worldlinknext)
168         {
169                 if (l.goalentity.islinked)
170                 {
171                         if(DIFF_TEAM(l.goalentity, l.enemy))
172                         {
173                                 ons_debug(strcat(etos(l), " (link) is unshielding ", etos(l.enemy), " (point) because its team does not match ", etos(l.goalentity), " (point)\n"));
174                                 l.enemy.isshielded = false;
175                         }
176                         if(l.goalentity.classname == "onslaught_generator")
177                                 l.enemy.isgenneighbor[l.goalentity.team] = true;
178                         else
179                                 l.enemy.iscpneighbor[l.goalentity.team] = true;
180                 }
181                 if (l.enemy.islinked)
182                 {
183                         if(DIFF_TEAM(l.goalentity, l.enemy))
184                         {
185                                 ons_debug(strcat(etos(l), " (link) is unshielding ", etos(l.goalentity), " (point) because its team does not match ", etos(l.enemy), " (point)\n"));
186                                 l.goalentity.isshielded = false;
187                         }
188                         if(l.enemy.classname == "onslaught_generator")
189                                 l.goalentity.isgenneighbor[l.enemy.team] = true;
190                         else
191                                 l.goalentity.iscpneighbor[l.enemy.team] = true;
192                 }
193         }
194         // now update the generators
195         for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
196         {
197                 if (l.isshielded)
198                 {
199                         ons_debug(strcat(etos(l), " (generator) is shielded\n"));
200                         l.takedamage = DAMAGE_NO;
201                         l.bot_attack = false;
202                 }
203                 else
204                 {
205                         ons_debug(strcat(etos(l), " (generator) is not shielded\n"));
206                         l.takedamage = DAMAGE_AIM;
207                         l.bot_attack = true;
208                 }
209
210                 ons_Generator_UpdateSprite(l);
211         }
212         // now update the takedamage and alpha variables on control point icons
213         for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
214         {
215                 if (l.isshielded)
216                 {
217                         ons_debug(strcat(etos(l), " (point) is shielded\n"));
218                         if (l.goalentity)
219                         {
220                                 l.goalentity.takedamage = DAMAGE_NO;
221                                 l.goalentity.bot_attack = false;
222                         }
223                 }
224                 else
225                 {
226                         ons_debug(strcat(etos(l), " (point) is not shielded\n"));
227                         if (l.goalentity)
228                         {
229                                 l.goalentity.takedamage = DAMAGE_AIM;
230                                 l.goalentity.bot_attack = true;
231                         }
232                 }
233                 ons_ControlPoint_UpdateSprite(l);
234         }
235         l = findchain(classname, "ons_captureshield");
236         while(l)
237         {
238                 l.team = l.enemy.team;
239                 l.colormap = l.enemy.colormap;
240                 l = l.chain;
241         }
242 }
243
244
245 // ===================
246 // Main Link Functions
247 // ===================
248
249 bool ons_Link_Send(entity to, int sendflags)
250 {
251         WriteByte(MSG_ENTITY, ENT_CLIENT_RADARLINK);
252         WriteByte(MSG_ENTITY, sendflags);
253         if(sendflags & 1)
254         {
255                 WriteCoord(MSG_ENTITY, self.goalentity.origin_x);
256                 WriteCoord(MSG_ENTITY, self.goalentity.origin_y);
257                 WriteCoord(MSG_ENTITY, self.goalentity.origin_z);
258         }
259         if(sendflags & 2)
260         {
261                 WriteCoord(MSG_ENTITY, self.enemy.origin_x);
262                 WriteCoord(MSG_ENTITY, self.enemy.origin_y);
263                 WriteCoord(MSG_ENTITY, self.enemy.origin_z);
264         }
265         if(sendflags & 4)
266         {
267                 WriteByte(MSG_ENTITY, self.clientcolors); // which is goalentity's color + enemy's color * 16
268         }
269         return true;
270 }
271
272 void ons_Link_CheckUpdate()
273 {
274         // TODO check if the two sides have moved (currently they won't move anyway)
275         float cc = 0, cc1 = 0, cc2 = 0;
276         
277         if(self.goalentity.islinked || self.goalentity.iscaptured) { cc1 = (self.goalentity.team - 1) * 0x01; }
278         if(self.enemy.islinked || self.enemy.iscaptured) { cc2 = (self.enemy.team - 1) * 0x10; }
279         
280         cc = cc1 + cc2;
281
282         if(cc != self.clientcolors)
283         {
284                 self.clientcolors = cc;
285                 self.SendFlags |= 4;
286         }
287
288         self.nextthink = time;
289 }
290
291 void ons_DelayedLinkSetup()
292 {
293         self.goalentity = find(world, targetname, self.target);
294         self.enemy = find(world, targetname, self.target2);
295         if(!self.goalentity) { objerror("can not find target\n"); }
296         if(!self.enemy) { objerror("can not find target2\n"); }
297
298         ons_debug(strcat(etos(self.goalentity), " linked with ", etos(self.enemy), "\n"));
299         self.SendFlags |= 3;
300         self.think = ons_Link_CheckUpdate;
301         self.nextthink = time;
302 }
303
304
305 // =============================
306 // Main Control Point Functions
307 // =============================
308
309 int ons_ControlPoint_CanBeLinked(entity cp, int teamnumber)
310 {
311         if(cp.isgenneighbor[teamnumber]) { return 2; }
312         if(cp.iscpneighbor[teamnumber]) { return 1; }
313
314         return 0;
315 }
316
317 int ons_ControlPoint_Attackable(entity cp, int teamnumber)
318         // -2: SAME TEAM, attackable by enemy!
319         // -1: SAME TEAM!
320         // 0: off limits
321         // 1: attack it
322         // 2: touch it
323         // 3: attack it (HIGH PRIO)
324         // 4: touch it (HIGH PRIO)
325 {
326         int a;
327
328         if(cp.isshielded)
329         {
330                 return 0;
331         }
332         else if(cp.goalentity)
333         {
334                 // if there's already an icon built, nothing happens
335                 if(cp.team == teamnumber)
336                 {
337                         a = ons_ControlPoint_CanBeLinked(cp, teamnumber);
338                         if(a) // attackable by enemy?
339                                 return -2; // EMERGENCY!
340                         return -1;
341                 }
342                 // we know it can be linked, so no need to check
343                 // but...
344                 a = ons_ControlPoint_CanBeLinked(cp, teamnumber);
345                 if(a == 2) // near our generator?
346                         return 3; // EMERGENCY!
347                 return 1;
348         }
349         else
350         {
351                 // free point
352                 if(ons_ControlPoint_CanBeLinked(cp, teamnumber))
353                 {
354                         a = ons_ControlPoint_CanBeLinked(cp, teamnumber); // why was this here NUM_TEAM_1 + NUM_TEAM_2 - t
355                         if(a == 2)
356                                 return 4; // GET THIS ONE NOW!
357                         else
358                                 return 2; // TOUCH ME
359                 }
360         }
361         return 0;
362 }
363
364 void ons_ControlPoint_Icon_Damage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
365 {
366         entity oself;
367         
368         if(damage <= 0) { return; }
369
370         if (self.owner.isshielded)
371         {
372                 // this is protected by a shield, so ignore the damage
373                 if (time > self.pain_finished)
374                         if (IS_PLAYER(attacker))
375                         {
376                                 play2(attacker, "onslaught/damageblockedbyshield.wav");
377                                 self.pain_finished = time + 1;
378                                 attacker.typehitsound += 1; // play both sounds (shield is way too quiet)
379                         }
380
381                 return;
382         }
383
384         if(IS_PLAYER(attacker))
385         if(time - ons_notification_time[self.team] > 10)
386         {
387                 play2team(self.team, "onslaught/controlpoint_underattack.wav");
388                 ons_notification_time[self.team] = time;
389         }
390
391         self.health = self.health - damage;
392         if(self.owner.iscaptured)
393                 WaypointSprite_UpdateHealth(self.owner.sprite, self.health);
394         else
395                 WaypointSprite_UpdateBuildFinished(self.owner.sprite, time + (self.max_health - self.health) / (self.count / ONS_CP_THINKRATE));
396         self.pain_finished = time + 1;
397         // particles on every hit
398         pointparticles(particleeffectnum("sparks"), hitloc, force*-1, 1);
399         //sound on every hit
400         if (random() < 0.5)
401                 sound(self, CH_TRIGGER, "onslaught/ons_hit1.wav", VOL_BASE+0.3, ATTEN_NORM);
402         else
403                 sound(self, CH_TRIGGER, "onslaught/ons_hit2.wav", VOL_BASE+0.3, ATTEN_NORM);
404
405         if (self.health < 0)
406         {
407                 sound(self, CH_TRIGGER, "weapons/grenade_impact.wav", VOL_BASE, ATTEN_NORM);
408                 pointparticles(particleeffectnum("rocket_explode"), self.origin, '0 0 0', 1);
409                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(self.team, INFO_ONSLAUGHT_CPDESTROYED_), self.owner.message, attacker.netname);
410                 
411                 PlayerScore_Add(attacker, SP_ONS_TAKES, 1);
412                 PlayerScore_Add(attacker, SP_SCORE, 10);
413                 
414                 self.owner.goalentity = world;
415                 self.owner.islinked = false;
416                 self.owner.iscaptured = false;
417                 self.owner.team = 0;
418                 self.owner.colormap = 1024;
419
420                 WaypointSprite_UpdateMaxHealth(self.owner.sprite, 0);
421
422                 onslaught_updatelinks();
423
424                 // Use targets now (somebody make sure this is in the right place..)
425                 oself = self;
426                 self = self.owner;
427                 activator = self;
428                 SUB_UseTargets ();
429                 self = oself;
430
431                 self.owner.waslinked = self.owner.islinked;
432                 if(self.owner.model != "models/onslaught/controlpoint_pad.md3")
433                         setmodel_fixsize(self.owner, "models/onslaught/controlpoint_pad.md3");
434                 //setsize(self, '-32 -32 0', '32 32 8');
435
436                 remove(self);
437         }
438         
439         self.SendFlags |= CPSF_STATUS;
440 }
441
442 void ons_ControlPoint_Icon_Think()
443 {
444         entity oself;
445         self.nextthink = time + ONS_CP_THINKRATE;
446
447         if(autocvar_g_onslaught_cp_proxydecap)
448         {
449         int _enemy_count = 0;
450         int _friendly_count = 0;
451         float _dist;
452         entity _player;
453
454         FOR_EACH_PLAYER(_player)
455         {
456             if(!_player.deadflag)
457             {
458                 _dist = vlen(_player.origin - self.origin);
459                 if(_dist < autocvar_g_onslaught_cp_proxydecap_distance)
460                 {
461                                         if(SAME_TEAM(_player, self))
462                         ++_friendly_count;
463                     else
464                         ++_enemy_count;
465                 }
466             }
467         }
468
469         _friendly_count = _friendly_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE);
470         _enemy_count = _enemy_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE);
471
472         self.health = bound(0, self.health + (_friendly_count - _enemy_count), self.max_health);
473                 self.SendFlags |= CPSF_STATUS;
474         if(self.health <= 0)
475         {
476             ons_ControlPoint_Icon_Damage(self, self, 1, 0, self.origin, '0 0 0');
477             return;
478         }
479     }
480
481         if (time > self.pain_finished + 5)
482         {
483                 if(self.health < self.max_health)
484                 {
485                         self.health = self.health + self.count;
486                         if (self.health >= self.max_health)
487                                 self.health = self.max_health;
488                         WaypointSprite_UpdateHealth(self.owner.sprite, self.health);
489                 }
490         }
491
492         if(self.owner.islinked != self.owner.waslinked)
493         {
494                 // unteam the spawnpoint if needed
495                 int t = self.owner.team;
496                 if(!self.owner.islinked)
497                         self.owner.team = 0;
498
499                 oself = self;
500                 self = self.owner;
501                 activator = self;
502                 SUB_UseTargets ();
503                 self = oself;
504
505                 self.owner.team = t;
506
507                 self.owner.waslinked = self.owner.islinked;
508         }
509
510         // damaged fx
511         if(random() < 0.6 - self.health / self.max_health)
512         {
513                 pointparticles(particleeffectnum("electricity_sparks"), self.origin + randompos('-10 -10 -20', '10 10 20'), '0 0 0', 1);
514
515                 if(random() > 0.8)
516                         sound(self, CH_PAIN, "onslaught/ons_spark1.wav", VOL_BASE, ATTEN_NORM);
517                 else if (random() > 0.5)
518                         sound(self, CH_PAIN, "onslaught/ons_spark2.wav", VOL_BASE, ATTEN_NORM);
519         }
520 }
521
522 void ons_ControlPoint_Icon_BuildThink()
523 {
524         entity oself;
525         int a;
526
527         self.nextthink = time + ONS_CP_THINKRATE;
528
529         // only do this if there is power
530         a = ons_ControlPoint_CanBeLinked(self.owner, self.owner.team);
531         if(!a)
532                 return;
533
534         self.health = self.health + self.count;
535         
536         self.SendFlags |= CPSF_STATUS;
537
538         if (self.health >= self.max_health)
539         {
540                 self.health = self.max_health;
541                 self.count = autocvar_g_onslaught_cp_regen * ONS_CP_THINKRATE; // slow repair rate from now on
542                 self.think = ons_ControlPoint_Icon_Think;
543                 sound(self, CH_TRIGGER, "onslaught/controlpoint_built.wav", VOL_BASE, ATTEN_NORM);
544                 self.owner.iscaptured = true;
545                 self.solid = SOLID_BBOX;
546
547                 pointparticles(particleeffectnum(sprintf("%s_cap", Static_Team_ColorName_Lower(self.owner.team))), self.owner.origin, '0 0 0', 1);
548
549                 WaypointSprite_UpdateMaxHealth(self.owner.sprite, self.max_health);
550                 WaypointSprite_UpdateHealth(self.owner.sprite, self.health);
551
552                 if(IS_PLAYER(self.owner.ons_toucher))
553                 {
554                         Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ONSLAUGHT_CAPTURE, self.owner.ons_toucher.netname, self.owner.message);
555                         Send_Notification(NOTIF_ALL_EXCEPT, self.owner.ons_toucher, MSG_CENTER, APP_TEAM_ENT_4(self.owner.ons_toucher, CENTER_ONS_CAPTURE_), self.owner.message);
556                         Send_Notification(NOTIF_ONE, self.owner.ons_toucher, MSG_CENTER, CENTER_ONS_CAPTURE, self.owner.message);
557                         PlayerScore_Add(self.owner.ons_toucher, SP_ONS_CAPS, 1);
558                         PlayerTeamScore_AddScore(self.owner.ons_toucher, 10);
559                 }
560                 
561                 self.owner.ons_toucher = world;
562
563                 onslaught_updatelinks();
564
565                 // Use targets now (somebody make sure this is in the right place..)
566                 oself = self;
567                 self = self.owner;
568                 activator = self;
569                 SUB_UseTargets ();
570                 self = oself;
571                 
572                 self.SendFlags |= CPSF_SETUP;
573         }
574         if(self.owner.model != "models/onslaught/controlpoint_pad2.md3")
575                 setmodel_fixsize(self.owner, "models/onslaught/controlpoint_pad2.md3");
576                 
577         if(random() < 0.9 - self.health / self.max_health)
578                 pointparticles(particleeffectnum("rage"), self.origin + 10 * randomvec(), '0 0 -1', 1);
579 }
580
581 void ons_ControlPoint_Icon_Spawn(entity cp, entity player)
582 {
583         entity e = spawn();
584         
585         setsize(e, CPICON_MIN, CPICON_MAX);
586         setorigin(e, cp.origin + CPICON_OFFSET);
587         
588         e.classname = "onslaught_controlpoint_icon";
589         e.owner = cp;
590         e.max_health = autocvar_g_onslaught_cp_health;
591         e.health = autocvar_g_onslaught_cp_buildhealth;
592         e.solid = SOLID_NOT;
593         e.takedamage = DAMAGE_AIM;
594         e.bot_attack = true;
595         e.event_damage = ons_ControlPoint_Icon_Damage;
596         e.team = player.team;
597         e.colormap = 1024 + (e.team - 1) * 17;
598         e.count = (e.max_health - e.health) * ONS_CP_THINKRATE / autocvar_g_onslaught_cp_buildtime; // how long it takes to build
599         
600         sound(e, CH_TRIGGER, "onslaught/controlpoint_build.wav", VOL_BASE, ATTEN_NORM);
601         
602         cp.goalentity = e;
603         cp.team = e.team;
604         cp.colormap = e.colormap;
605
606         pointparticles(particleeffectnum(sprintf("%sflag_touch", Static_Team_ColorName_Lower(player.team))), e.origin, '0 0 0', 1);
607
608         WaypointSprite_UpdateBuildFinished(cp.sprite, time + (e.max_health - e.health) / (e.count / ONS_CP_THINKRATE));
609         WaypointSprite_UpdateRule(cp.sprite,cp.team,SPRITERULE_TEAMPLAY);
610         cp.sprite.SendFlags |= 16;
611
612         onslaught_controlpoint_icon_link(e, ons_ControlPoint_Icon_BuildThink);
613 }
614
615 string ons_ControlPoint_Waypoint(entity e)
616 {
617         if(e.team)
618         {
619                 int a = ons_ControlPoint_Attackable(e, e.team);
620                 
621                 if(a == -2) { return "ons-cp-dfnd"; } // defend now
622                 if(a == -1 || a == 1 || a == 2) { return "ons-cp"; } // touch
623                 if(a == 3 || a == 4) { return "ons-cp-atck"; } // attack
624         }
625         else
626                 return "ons-cp";
627
628         return "";
629 }
630
631 void ons_ControlPoint_UpdateSprite(entity e)
632 {
633         string s1;
634         s1 = ons_ControlPoint_Waypoint(e);
635         WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1);
636
637         bool sh;
638         sh = !(ons_ControlPoint_CanBeLinked(e, NUM_TEAM_1) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_2) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_3) || ons_ControlPoint_CanBeLinked(e, NUM_TEAM_4));
639
640         if(e.lastteam != e.team + 2 || e.lastshielded != sh || e.iscaptured != e.lastcaptured)
641         {
642                 if(e.iscaptured) // don't mess up build bars!
643                 {
644                         if(sh)
645                         {
646                                 WaypointSprite_UpdateMaxHealth(e.sprite, 0);
647                         }
648                         else
649                         {
650                                 WaypointSprite_UpdateMaxHealth(e.sprite, e.goalentity.max_health);
651                                 WaypointSprite_UpdateHealth(e.sprite, e.goalentity.health);
652                         }
653                 }
654                 if(e.lastshielded)
655                 {
656                         if(e.team)
657                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, 0.5 * colormapPaletteColor(e.team - 1, false));
658                         else
659                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.5 0.5 0.5');
660                 }
661                 else
662                 {
663                         if(e.team)
664                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, colormapPaletteColor(e.team - 1, false));
665                         else
666                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.75 0.75 0.75');
667                 }
668                 WaypointSprite_Ping(e.sprite);
669
670                 e.lastteam = e.team + 2;
671                 e.lastshielded = sh;
672                 e.lastcaptured = e.iscaptured;
673         }
674 }
675
676 void ons_ControlPoint_Touch()
677 {
678         entity toucher = other;
679         int attackable;
680         
681         if((toucher.vehicle_flags & VHF_ISVEHICLE) && toucher.owner)
682         if(autocvar_g_onslaught_allow_vehicle_touch)
683                 toucher = toucher.owner;
684         else
685                 return;
686                 
687         if(!IS_PLAYER(toucher)) { return; }
688         if(toucher.frozen) { return; }
689         if(toucher.deadflag != DEAD_NO) { return; }
690         
691         if ( SAME_TEAM(self,toucher) )
692         if ( self.iscaptured )
693         {
694                 if(time <= toucher.teleport_antispam)
695                         Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT_ANTISPAM, rint(toucher.teleport_antispam - time));
696                 else
697                         Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT);
698         }
699                 
700         attackable = ons_ControlPoint_Attackable(self, toucher.team);
701         if(attackable != 2 && attackable != 4)
702                 return;
703         // we've verified that this player has a legitimate claim to this point,
704         // so start building the captured point icon (which only captures this
705         // point if it successfully builds without being destroyed first)
706         ons_ControlPoint_Icon_Spawn(self, toucher);
707         
708         self.ons_toucher = toucher;
709
710         onslaught_updatelinks();
711 }
712
713 void ons_ControlPoint_Think()
714 {
715         self.nextthink = time + ONS_CP_THINKRATE;
716         CSQCMODEL_AUTOUPDATE();
717 }
718
719 void ons_ControlPoint_Reset()
720 {
721         if(self.goalentity)
722                 remove(self.goalentity);
723
724         self.goalentity = world;
725         self.team = 0;
726         self.colormap = 1024;
727         self.iscaptured = false;
728         self.islinked = false;
729         self.isshielded = true;
730         self.think = ons_ControlPoint_Think;
731         self.ons_toucher = world;
732         self.nextthink = time + ONS_CP_THINKRATE;
733         setmodel_fixsize(self, "models/onslaught/controlpoint_pad.md3");
734
735         WaypointSprite_UpdateMaxHealth(self.sprite, 0);
736         WaypointSprite_UpdateRule(self.sprite,self.team,SPRITERULE_TEAMPLAY);
737
738         onslaught_updatelinks();
739
740         activator = self;
741         SUB_UseTargets(); // to reset the structures, playerspawns etc.
742
743         CSQCMODEL_AUTOUPDATE();
744 }
745
746 void ons_DelayedControlPoint_Setup(void)
747 {
748         onslaught_updatelinks();
749         
750         // captureshield setup
751         ons_CaptureShield_Spawn(self, false);
752
753         CSQCMODEL_AUTOINIT();
754 }
755
756 void ons_ControlPoint_Setup(entity cp)
757 {
758         // declarations
759         self = cp; // for later usage with droptofloor()
760         
761         // main setup
762         cp.ons_worldcpnext = ons_worldcplist; // link control point into ons_worldcplist
763         ons_worldcplist = cp;
764         
765         cp.netname = "Control point";
766         cp.team = 0;
767         cp.solid = SOLID_BBOX;
768         cp.movetype = MOVETYPE_NONE;
769         cp.touch = ons_ControlPoint_Touch;
770         cp.think = ons_ControlPoint_Think;
771         cp.nextthink = time + ONS_CP_THINKRATE;
772         cp.reset = ons_ControlPoint_Reset;
773         cp.colormap = 1024;
774         cp.iscaptured = false;
775         cp.islinked = false;
776         cp.isshielded = true;
777         
778         if(cp.message == "") { cp.message = "a"; }
779
780         // precache - TODO: clean up!
781         precache_model("models/onslaught/controlpoint_pad.md3");
782         precache_model("models/onslaught/controlpoint_pad2.md3");
783         precache_model("models/onslaught/controlpoint_shield.md3");
784         precache_model("models/onslaught/controlpoint_icon.md3");
785         precache_model("models/onslaught/controlpoint_icon_dmg1.md3");
786         precache_model("models/onslaught/controlpoint_icon_dmg2.md3");
787         precache_model("models/onslaught/controlpoint_icon_dmg3.md3");
788         precache_model("models/onslaught/controlpoint_icon_gib1.md3");
789         precache_model("models/onslaught/controlpoint_icon_gib2.md3");
790         precache_model("models/onslaught/controlpoint_icon_gib4.md3");
791         precache_sound("onslaught/controlpoint_build.wav");
792         precache_sound("onslaught/controlpoint_built.wav");
793         precache_sound("weapons/grenade_impact.wav");
794         precache_sound("onslaught/damageblockedbyshield.wav");
795         precache_sound("onslaught/controlpoint_underattack.wav");
796         precache_sound("onslaught/ons_spark1.wav");
797         precache_sound("onslaught/ons_spark2.wav");
798         
799         // appearence
800         setmodel_fixsize(cp, "models/onslaught/controlpoint_pad.md3");
801         
802         // control point placement
803         if((cp.spawnflags & 1) || cp.noalign) // don't drop to floor, just stay at fixed location
804         {
805                 cp.noalign = true;
806                 cp.movetype = MOVETYPE_NONE;
807         }
808         else // drop to floor, automatically find a platform and set that as spawn origin
809         {
810                 setorigin(cp, cp.origin + '0 0 20');
811                 cp.noalign = false;
812                 self = cp;
813                 droptofloor();
814                 cp.movetype = MOVETYPE_TOSS;
815         }
816         
817         // waypointsprites
818         WaypointSprite_SpawnFixed(string_null, self.origin + CPGEN_WAYPOINT_OFFSET, self, sprite, RADARICON_NONE, '0 0 0');
819         WaypointSprite_UpdateRule(self.sprite, self.team, SPRITERULE_TEAMPLAY);
820         
821         InitializeEntity(cp, ons_DelayedControlPoint_Setup, INITPRIO_SETLOCATION);
822 }
823
824
825 // =========================
826 // Main Generator Functions
827 // =========================
828
829 string ons_Generator_Waypoint(entity e)
830 {
831         if(e.isshielded)
832                 return "ons-gen-shielded";
833         return "ons-gen";
834 }
835
836 void ons_Generator_UpdateSprite(entity e)
837 {
838         string s1 = ons_Generator_Waypoint(e);
839         WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1);
840
841         if(e.lastteam != e.team + 2 || e.lastshielded != e.isshielded)
842         {
843                 e.lastteam = e.team + 2;
844                 e.lastshielded = e.isshielded;
845                 if(e.lastshielded)
846                 {
847                         if(e.team)
848                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, 0.5 * colormapPaletteColor(e.team - 1, false));
849                         else
850                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.5 0.5 0.5');
851                 }
852                 else
853                 {
854                         if(e.team)
855                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, colormapPaletteColor(e.team - 1, false));
856                         else
857                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.75 0.75 0.75');
858                 }
859                 WaypointSprite_Ping(e.sprite);
860         }
861 }
862
863 void ons_GeneratorDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
864 {
865         if(damage <= 0) { return; }
866         if(warmup_stage || gameover) { return; }
867         if(!round_handler_IsRoundStarted()) { return; }
868
869         if (attacker != self)
870         {
871                 if (self.isshielded)
872                 {
873                         // this is protected by a shield, so ignore the damage
874                         if (time > self.pain_finished)
875                                 if (IS_PLAYER(attacker))
876                                 {
877                                         play2(attacker, "onslaught/damageblockedbyshield.wav");
878                                         attacker.typehitsound += 1;
879                                         self.pain_finished = time + 1;
880                                 }
881                         return;
882                 }
883                 if (time > self.pain_finished)
884                 {
885                         self.pain_finished = time + 10;
886                         entity head;
887                         FOR_EACH_REALPLAYER(head) if(SAME_TEAM(head, self)) { Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_GENERATOR_UNDERATTACK); }
888                         play2team(self.team, "onslaught/generator_underattack.wav");
889                 }
890         }
891         self.health = self.health - damage;
892         WaypointSprite_UpdateHealth(self.sprite, self.health);
893         // choose an animation frame based on health
894         self.frame = 10 * bound(0, (1 - self.health / self.max_health), 1);
895         // see if the generator is still functional, or dying
896         if (self.health > 0)
897         {
898                 self.lasthealth = self.health;
899         }
900         else
901         {
902                 if (attacker == self)
903                         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(self.team, INFO_ONSLAUGHT_GENDESTROYED_OVERTIME_));
904                 else
905                 {
906                         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(self.team, INFO_ONSLAUGHT_GENDESTROYED_));
907                         PlayerScore_Add(attacker, SP_SCORE, 100);
908                 }
909                 self.iscaptured = false;
910                 self.islinked = false;
911                 self.isshielded = false;
912                 self.takedamage = DAMAGE_NO; // can't be hurt anymore
913                 self.event_damage = func_null; // won't do anything if hurt
914                 self.count = 0; // reset counter
915                 self.think = func_null;
916                 self.nextthink = 0;
917                 //self.think(); // do the first explosion now
918
919                 WaypointSprite_UpdateMaxHealth(self.sprite, 0);
920                 WaypointSprite_Ping(self.sprite);
921                 //WaypointSprite_Kill(self.sprite); // can't do this yet, code too poor
922
923                 onslaught_updatelinks();
924         }
925
926         // Throw some flaming gibs on damage, more damage = more chance for gib
927         if(random() < damage/220)
928         {
929                 sound(self, CH_TRIGGER, "weapons/rocket_impact.wav", VOL_BASE, ATTEN_NORM);
930         }
931         else
932         {
933                 // particles on every hit
934                 pointparticles(particleeffectnum("sparks"), hitloc, force * -1, 1);
935
936                 //sound on every hit
937                 if (random() < 0.5)
938                         sound(self, CH_TRIGGER, "onslaught/ons_hit1.wav", VOL_BASE, ATTEN_NORM);
939                 else
940                         sound(self, CH_TRIGGER, "onslaught/ons_hit2.wav", VOL_BASE, ATTEN_NORM);
941         }
942
943         self.SendFlags |= GSF_STATUS;
944 }
945
946 void ons_GeneratorThink()
947 {
948         entity e;
949         self.nextthink = time + GEN_THINKRATE;
950         if (!gameover)
951         {
952         if(!self.isshielded && self.wait < time)
953         {
954             self.wait = time + 5;
955             FOR_EACH_REALPLAYER(e)
956             {
957                                 if(SAME_TEAM(e, self))
958                                 {
959                                         Send_Notification(NOTIF_ONE, e, MSG_CENTER, CENTER_ONS_NOTSHIELDED_TEAM);
960                     soundto(MSG_ONE, e, CHAN_AUTO, "kh/alarm.wav", VOL_BASE, ATTEN_NONE);    // FIXME: unique sound?
961                 }
962                                 else
963                                         Send_Notification(NOTIF_ONE, e, MSG_CENTER, APP_TEAM_NUM_4(self.team, CENTER_ONS_NOTSHIELDED_));
964             }
965         }
966         }
967 }
968
969 void ons_GeneratorReset()
970 {
971         self.team = self.team_saved;
972         self.lasthealth = self.max_health = self.health = autocvar_g_onslaught_gen_health;
973         self.takedamage = DAMAGE_AIM;
974         self.bot_attack = true;
975         self.iscaptured = true;
976         self.islinked = true;
977         self.isshielded = true;
978         self.event_damage = ons_GeneratorDamage;
979         self.think = ons_GeneratorThink;
980         self.nextthink = time + GEN_THINKRATE;
981         
982         Net_LinkEntity(self, false, 0, generator_send);
983         
984         self.SendFlags = GSF_SETUP; // just incase
985         self.SendFlags |= GSF_STATUS;
986
987         WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
988         WaypointSprite_UpdateHealth(self.sprite, self.health);
989         WaypointSprite_UpdateRule(self.sprite,self.team,SPRITERULE_TEAMPLAY);
990         
991         onslaught_updatelinks();
992 }
993
994 void ons_DelayedGeneratorSetup()
995 {
996         // bot waypoints
997         waypoint_spawnforitem_force(self, self.origin);
998         self.nearestwaypointtimeout = 0; // activate waypointing again
999         self.bot_basewaypoint = self.nearestwaypoint;
1000
1001         // captureshield setup
1002         ons_CaptureShield_Spawn(self, true);
1003         
1004         onslaught_updatelinks();
1005         
1006         Net_LinkEntity(self, false, 0, generator_send);
1007 }
1008
1009
1010 void onslaught_generator_touch()
1011 {
1012         if ( IS_PLAYER(other) )
1013         if ( SAME_TEAM(self,other) )
1014         if ( self.iscaptured )
1015         {
1016                 Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_ONS_TELEPORT);
1017         }
1018 }
1019
1020 void ons_GeneratorSetup(entity gen) // called when spawning a generator entity on the map as a spawnfunc
1021 {
1022         // declarations
1023         int teamnumber = gen.team;
1024         self = gen; // for later usage with droptofloor()
1025         
1026         // main setup
1027         gen.ons_worldgeneratornext = ons_worldgeneratorlist; // link generator into ons_worldgeneratorlist
1028         ons_worldgeneratorlist = gen;
1029         
1030         gen.netname = sprintf("%s generator", Team_ColoredFullName(teamnumber));
1031         gen.classname = "onslaught_generator";
1032         gen.solid = SOLID_BBOX;
1033         gen.team_saved = teamnumber;
1034         gen.movetype = MOVETYPE_NONE;
1035         gen.lasthealth = gen.max_health = gen.health = autocvar_g_onslaught_gen_health;
1036         gen.takedamage = DAMAGE_AIM;
1037         gen.bot_attack = true;
1038         gen.event_damage = ons_GeneratorDamage;
1039         gen.reset = ons_GeneratorReset;
1040         gen.think = ons_GeneratorThink;
1041         gen.nextthink = time + GEN_THINKRATE;
1042         gen.iscaptured = true;
1043         gen.islinked = true;
1044         gen.isshielded = true;
1045         gen.touch = onslaught_generator_touch;
1046         
1047         // precache - TODO: clean up!
1048         precache_model("models/onslaught/generator_shield.md3");
1049         precache_model("models/onslaught/gen_gib1.md3");
1050         precache_model("models/onslaught/gen_gib2.md3");
1051         precache_model("models/onslaught/gen_gib3.md3");
1052         precache_sound("onslaught/generator_decay.wav");
1053         precache_sound("weapons/grenade_impact.wav");
1054         precache_sound("weapons/rocket_impact.wav");
1055         precache_sound("onslaught/generator_underattack.wav");
1056         precache_sound("onslaught/shockwave.wav");
1057         precache_sound("onslaught/ons_hit1.wav");
1058         precache_sound("onslaught/ons_hit2.wav");
1059         precache_sound("onslaught/generator_underattack.wav");
1060         
1061         // appearence
1062         // model handled by CSQC
1063         setsize(gen, GENERATOR_MIN, GENERATOR_MAX);
1064         setorigin(gen, (gen.origin + CPGEN_SPAWN_OFFSET));
1065         gen.colormap = 1024 + (teamnumber - 1) * 17;
1066         
1067         // generator placement
1068         self = gen;
1069         droptofloor();
1070         
1071         // waypointsprites
1072         WaypointSprite_SpawnFixed(string_null, self.origin + CPGEN_WAYPOINT_OFFSET, self, sprite, RADARICON_NONE, '0 0 0');
1073         WaypointSprite_UpdateRule(self.sprite, self.team, SPRITERULE_TEAMPLAY);
1074         WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
1075         WaypointSprite_UpdateHealth(self.sprite, self.health);
1076         
1077         InitializeEntity(gen, ons_DelayedGeneratorSetup, INITPRIO_SETLOCATION);
1078 }
1079
1080
1081 // ===============
1082 //  Round Handler
1083 // ===============
1084
1085 int total_generators;
1086 void Onslaught_count_generators()
1087 {
1088         entity e;
1089         total_generators = redowned = blueowned = yellowowned = pinkowned = 0;
1090         for(e = ons_worldgeneratorlist; e; e = e.ons_worldgeneratornext)
1091         {
1092                 ++total_generators;
1093                 redowned += (e.team == NUM_TEAM_1 && e.health > 0);
1094                 blueowned += (e.team == NUM_TEAM_2 && e.health > 0);
1095                 yellowowned += (e.team == NUM_TEAM_3 && e.health > 0);
1096                 pinkowned += (e.team == NUM_TEAM_4 && e.health > 0);
1097         }
1098 }
1099
1100 int Onslaught_GetWinnerTeam()
1101 {
1102         int winner_team = 0;
1103         if(redowned > 0)
1104                 winner_team = NUM_TEAM_1;
1105         if(blueowned > 0)
1106         {
1107                 if(winner_team) return 0;
1108                 winner_team = NUM_TEAM_2;
1109         }
1110         if(yellowowned > 0)
1111         {
1112                 if(winner_team) return 0;
1113                 winner_team = NUM_TEAM_3;
1114         }
1115         if(pinkowned > 0)
1116         {
1117                 if(winner_team) return 0;
1118                 winner_team = NUM_TEAM_4;
1119         }
1120         if(winner_team)
1121                 return winner_team;
1122         return -1; // no generators left?
1123 }
1124
1125 #define ONS_OWNED_GENERATORS() ((redowned > 0) + (blueowned > 0) + (yellowowned > 0) + (pinkowned > 0))
1126 #define ONS_OWNED_GENERATORS_OK() (ONS_OWNED_GENERATORS() > 1)
1127 bool Onslaught_CheckWinner()
1128 {
1129         entity e;
1130         
1131         if ((autocvar_timelimit && time > game_starttime + autocvar_timelimit * 60) || (round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0))
1132         {
1133                 ons_stalemate = true;
1134
1135                 if (!wpforenemy_announced)
1136                 {
1137                         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT);
1138                         sound(world, CH_INFO, "onslaught/generator_decay.wav", VOL_BASE, ATTEN_NONE);
1139
1140                         wpforenemy_announced = true;
1141                 }
1142
1143                 entity tmp_entity; // temporary entity
1144                 float d;
1145                 for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext) if(time >= tmp_entity.ons_overtime_damagedelay)
1146                 {
1147                         // tmp_entity.max_health / 300 gives 5 minutes of overtime.
1148                         // control points reduce the overtime duration.
1149                         d = 1;
1150                         for(e = ons_worldcplist; e; e = e.ons_worldcpnext)
1151                         {
1152                                 if(DIFF_TEAM(e, tmp_entity))
1153                                 if(e.islinked)
1154                                         d = d + 1;
1155                         }
1156
1157                         if(autocvar_g_campaign && autocvar__campaign_testrun)
1158                                 d = d * tmp_entity.max_health;
1159                         else
1160                                 d = d * tmp_entity.max_health / max(30, 60 * autocvar_timelimit_suddendeath);
1161
1162                         Damage(tmp_entity, tmp_entity, tmp_entity, d, DEATH_HURTTRIGGER, tmp_entity.origin, '0 0 0');
1163                         
1164                         tmp_entity.sprite.SendFlags |= 16;
1165
1166                         tmp_entity.ons_overtime_damagedelay = time + 1;
1167                 }
1168         }
1169         else { wpforenemy_announced = false; ons_stalemate = false; }
1170
1171         Onslaught_count_generators();
1172         
1173         if(ONS_OWNED_GENERATORS_OK())
1174                 return 0;
1175
1176         int winner_team = Onslaught_GetWinnerTeam();
1177
1178         if(winner_team > 0)
1179         {
1180                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
1181                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
1182                 TeamScore_AddToTeam(winner_team, ST_ONS_CAPS, +1);
1183         }
1184         else if(winner_team == -1)
1185         {
1186                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
1187                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
1188         }
1189         
1190         ons_stalemate = false;
1191
1192         play2all(sprintf("ctf/%s_capture.wav", Static_Team_ColorName_Lower(winner_team)));
1193         
1194         round_handler_Init(7, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit);
1195         
1196         FOR_EACH_PLAYER(e)
1197         {
1198                 e.ons_roundlost = true;
1199                 e.player_blocked = true;
1200
1201                 nades_Clear(e);
1202         }
1203
1204         return 1;
1205 }
1206
1207 bool Onslaught_CheckPlayers()
1208 {
1209         return 1;
1210 }
1211
1212 void Onslaught_RoundStart()
1213 {
1214         entity tmp_entity;
1215         FOR_EACH_PLAYER(tmp_entity) { tmp_entity.player_blocked = false; }
1216
1217         for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext)
1218                 tmp_entity.sprite.SendFlags |= 16;
1219
1220         for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
1221                 tmp_entity.sprite.SendFlags |= 16;
1222 }
1223
1224
1225 // ================
1226 // Bot player logic
1227 // ================
1228
1229 // NOTE: LEGACY CODE, needs to be re-written!
1230
1231 void havocbot_goalrating_ons_offenseitems(float ratingscale, vector org, float sradius)
1232 {
1233         entity head;
1234         float t, c;
1235         int i;
1236         bool needarmor = false, needweapons = false;
1237
1238         // Needs armor/health?
1239         if(self.health<100)
1240                 needarmor = true;
1241
1242         // Needs weapons?
1243         c = 0;
1244         for(i = WEP_FIRST; i <= WEP_LAST ; ++i)
1245         {
1246                 // Find weapon
1247                 if(self.weapons & WepSet_FromWeapon(i))
1248                 if(++c>=4)
1249                         break;
1250         }
1251
1252         if(c<4)
1253                 needweapons = true;
1254
1255         if(!needweapons && !needarmor)
1256                 return;
1257
1258         ons_debug(strcat(self.netname, " needs weapons ", ftos(needweapons) , "\n"));
1259         ons_debug(strcat(self.netname, " needs armor ", ftos(needarmor) , "\n"));
1260
1261         // See what is around
1262         head = findchainfloat(bot_pickup, true);
1263         while (head)
1264         {
1265                 // gather health and armor only
1266                 if (head.solid)
1267                 if ( ((head.health || head.armorvalue) && needarmor) || (head.weapons && needweapons ) )
1268                 if (vlen(head.origin - org) < sradius)
1269                 {
1270                         t = head.bot_pickupevalfunc(self, head);
1271                         if (t > 0)
1272                                 navigation_routerating(head, t * ratingscale, 500);
1273                 }
1274                 head = head.chain;
1275         }
1276 }
1277
1278 void havocbot_role_ons_setrole(entity bot, int role)
1279 {
1280         ons_debug(strcat(bot.netname," switched to "));
1281         switch(role)
1282         {
1283                 case HAVOCBOT_ONS_ROLE_DEFENSE:
1284                         ons_debug("defense");
1285                         bot.havocbot_role = havocbot_role_ons_defense;
1286                         bot.havocbot_role_flags = HAVOCBOT_ONS_ROLE_DEFENSE;
1287                         bot.havocbot_role_timeout = 0;
1288                         break;
1289                 case HAVOCBOT_ONS_ROLE_ASSISTANT:
1290                         ons_debug("assistant");
1291                         bot.havocbot_role = havocbot_role_ons_assistant;
1292                         bot.havocbot_role_flags = HAVOCBOT_ONS_ROLE_ASSISTANT;
1293                         bot.havocbot_role_timeout = 0;
1294                         break;
1295                 case HAVOCBOT_ONS_ROLE_OFFENSE:
1296                         ons_debug("offense");
1297                         bot.havocbot_role = havocbot_role_ons_offense;
1298                         bot.havocbot_role_flags = HAVOCBOT_ONS_ROLE_OFFENSE;
1299                         bot.havocbot_role_timeout = 0;
1300                         break;
1301         }
1302         ons_debug("\n");
1303 }
1304
1305 int havocbot_ons_teamcount(entity bot, int role)
1306 {
1307         int c = 0;
1308         entity head;
1309
1310         FOR_EACH_PLAYER(head)
1311         if(SAME_TEAM(head, self))
1312         if(head.havocbot_role_flags & role)
1313                 ++c;
1314
1315         return c;
1316 }
1317
1318 void havocbot_goalrating_ons_controlpoints_attack(float ratingscale)
1319 {
1320         entity cp, cp1, cp2, best, pl, wp;
1321         float radius, bestvalue;
1322         int c;
1323         bool found;
1324
1325         // Filter control points
1326         for(cp2 = ons_worldcplist; cp2; cp2 = cp2.ons_worldcpnext)
1327         {
1328                 cp2.wpcost = c = 0;
1329                 cp2.wpconsidered = false;
1330
1331                 if(cp2.isshielded)
1332                         continue;
1333
1334                 // Ignore owned controlpoints
1335                 if(!(cp2.isgenneighbor[self.team] || cp2.iscpneighbor[self.team]))
1336                         continue;
1337
1338                 // Count team mates interested in this control point
1339                 // (easier and cleaner than keeping counters per cp and teams)
1340                 FOR_EACH_PLAYER(pl)
1341                 if(SAME_TEAM(pl, self))
1342                 if(pl.havocbot_role_flags & HAVOCBOT_ONS_ROLE_OFFENSE)
1343                 if(pl.havocbot_ons_target==cp2)
1344                         ++c;
1345
1346                 // NOTE: probably decrease the cost of attackable control points
1347                 cp2.wpcost = c;
1348                 cp2.wpconsidered = true;
1349         }
1350
1351         // We'll consider only the best case
1352         bestvalue = 99999999999;
1353         cp = world;
1354         for(cp1 = ons_worldcplist; cp1; cp1 = cp1.ons_worldcpnext)
1355         {
1356                 if (!cp1.wpconsidered)
1357                         continue;
1358
1359                 if(cp1.wpcost<bestvalue)
1360                 {
1361                         bestvalue = cp1.wpcost;
1362                         cp = cp1;
1363                         self.havocbot_ons_target = cp1;
1364                 }
1365         }
1366
1367         if (!cp)
1368                 return;
1369
1370         ons_debug(strcat(self.netname, " chose cp ranked ", ftos(bestvalue), "\n"));
1371
1372         if(cp.goalentity)
1373         {
1374                 // Should be attacked
1375                 // Rate waypoints near it
1376                 found = false;
1377                 best = world;
1378                 bestvalue = 99999999999;
1379                 for(radius=0; radius<1000 && !found; radius+=500)
1380                 {
1381                         for(wp=findradius(cp.origin,radius); wp; wp=wp.chain)
1382                         {
1383                                 if(!(wp.wpflags & WAYPOINTFLAG_GENERATED))
1384                                 if(wp.classname=="waypoint")
1385                                 if(checkpvs(wp.origin,cp))
1386                                 {
1387                                         found = true;
1388                                         if(wp.cnt<bestvalue)
1389                                         {
1390                                                 best = wp;
1391                                                 bestvalue = wp.cnt;
1392                                         }
1393                                 }
1394                         }
1395                 }
1396
1397                 if(best)
1398                 {
1399                         navigation_routerating(best, ratingscale, 10000);
1400                         best.cnt += 1;
1401
1402                         self.havocbot_attack_time = 0;
1403                         if(checkpvs(self.view_ofs,cp))
1404                         if(checkpvs(self.view_ofs,best))
1405                                 self.havocbot_attack_time = time + 2;
1406                 }
1407                 else
1408                 {
1409                         navigation_routerating(cp, ratingscale, 10000);
1410                 }
1411                 ons_debug(strcat(self.netname, " found an attackable controlpoint at ", vtos(cp.origin) ,"\n"));
1412         }
1413         else
1414         {
1415                 // Should be touched
1416                 ons_debug(strcat(self.netname, " found a touchable controlpoint at ", vtos(cp.origin) ,"\n"));
1417                 found = false;
1418
1419                 // Look for auto generated waypoint
1420                 if (!bot_waypoints_for_items)
1421                 for (wp = findradius(cp.origin,100); wp; wp = wp.chain)
1422                 {
1423                         if(wp.classname=="waypoint")
1424                         {
1425                                 navigation_routerating(wp, ratingscale, 10000);
1426                                 found = true;
1427                         }
1428                 }
1429
1430                 // Nothing found, rate the controlpoint itself
1431                 if (!found)
1432                         navigation_routerating(cp, ratingscale, 10000);
1433         }
1434 }
1435
1436 bool havocbot_goalrating_ons_generator_attack(float ratingscale)
1437 {
1438         entity g, wp, bestwp;
1439         bool found;
1440         int best;
1441
1442         for(g = ons_worldgeneratorlist; g; g = g.ons_worldgeneratornext)
1443         {
1444                 if(SAME_TEAM(g, self) || g.isshielded)
1445                         continue;
1446
1447                 // Should be attacked
1448                 // Rate waypoints near it
1449                 found = false;
1450                 bestwp = world;
1451                 best = 99999999999;
1452
1453                 for(wp=findradius(g.origin,400); wp; wp=wp.chain)
1454                 {
1455                         if(wp.classname=="waypoint")
1456                         if(checkpvs(wp.origin,g))
1457                         {
1458                                 found = true;
1459                                 if(wp.cnt<best)
1460                                 {
1461                                         bestwp = wp;
1462                                         best = wp.cnt;
1463                                 }
1464                         }
1465                 }
1466
1467                 if(bestwp)
1468                 {
1469                         ons_debug("waypoints found around generator\n");
1470                         navigation_routerating(bestwp, ratingscale, 10000);
1471                         bestwp.cnt += 1;
1472
1473                         self.havocbot_attack_time = 0;
1474                         if(checkpvs(self.view_ofs,g))
1475                         if(checkpvs(self.view_ofs,bestwp))
1476                                 self.havocbot_attack_time = time + 5;
1477
1478                         return true;
1479                 }
1480                 else
1481                 {
1482                         ons_debug("generator found without waypoints around\n");
1483                         // if there aren't waypoints near the generator go straight to it
1484                         navigation_routerating(g, ratingscale, 10000);
1485                         self.havocbot_attack_time = 0;
1486                         return true;
1487                 }
1488         }
1489         return false;
1490 }
1491
1492 void havocbot_role_ons_offense()
1493 {
1494         if(self.deadflag != DEAD_NO)
1495         {
1496                 self.havocbot_attack_time = 0;
1497                 havocbot_ons_reset_role(self);
1498                 return;
1499         }
1500
1501         // Set the role timeout if necessary
1502         if (!self.havocbot_role_timeout)
1503                 self.havocbot_role_timeout = time + 120;
1504
1505         if (time > self.havocbot_role_timeout)
1506         {
1507                 havocbot_ons_reset_role(self);
1508                 return;
1509         }
1510
1511         if(self.havocbot_attack_time>time)
1512                 return;
1513
1514         if (self.bot_strategytime < time)
1515         {
1516                 navigation_goalrating_start();
1517                 havocbot_goalrating_enemyplayers(20000, self.origin, 650);
1518                 if(!havocbot_goalrating_ons_generator_attack(20000))
1519                         havocbot_goalrating_ons_controlpoints_attack(20000);
1520                 havocbot_goalrating_ons_offenseitems(10000, self.origin, 10000);
1521                 navigation_goalrating_end();
1522
1523                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1524         }
1525 }
1526
1527 void havocbot_role_ons_assistant()
1528 {
1529         havocbot_ons_reset_role(self);
1530 }
1531
1532 void havocbot_role_ons_defense()
1533 {
1534         havocbot_ons_reset_role(self);
1535 }
1536
1537 void havocbot_ons_reset_role(entity bot)
1538 {
1539         entity head;
1540         int c = 0;
1541
1542         if(self.deadflag != DEAD_NO)
1543                 return;
1544
1545         bot.havocbot_ons_target = world;
1546
1547         // TODO: Defend control points or generator if necessary
1548
1549         // if there is only me on the team switch to offense
1550         c = 0;
1551         FOR_EACH_PLAYER(head)
1552         if(SAME_TEAM(head, self))
1553                 ++c;
1554
1555         if(c==1)
1556         {
1557                 havocbot_role_ons_setrole(bot, HAVOCBOT_ONS_ROLE_OFFENSE);
1558                 return;
1559         }
1560
1561         havocbot_role_ons_setrole(bot, HAVOCBOT_ONS_ROLE_OFFENSE);
1562 }
1563
1564
1565 /*
1566  * Find control point or generator owned by the same team self which is nearest to pos
1567  * if max_dist is positive, only control points within this range will be considered
1568  */
1569 entity ons_Nearest_ControlPoint(vector pos, float max_dist)
1570 {
1571         entity tmp_entity, closest_target = world;
1572         tmp_entity = findchain(classname, "onslaught_controlpoint");
1573         while(tmp_entity)
1574         {
1575                 if(SAME_TEAM(tmp_entity, self))
1576                 if(tmp_entity.iscaptured)
1577                 if(max_dist <= 0 || vlen(tmp_entity.origin - pos) <= max_dist)
1578                 if(vlen(tmp_entity.origin - pos) <= vlen(closest_target.origin - pos) || closest_target == world)
1579                         closest_target = tmp_entity;
1580                 tmp_entity = tmp_entity.chain;
1581         }
1582         tmp_entity = findchain(classname, "onslaught_generator");
1583         while(tmp_entity)
1584         {
1585                 if(SAME_TEAM(tmp_entity, self))
1586                 if(max_dist <= 0 || vlen(tmp_entity.origin - pos) < max_dist)
1587                 if(vlen(tmp_entity.origin - pos) <= vlen(closest_target.origin - pos) || closest_target == world)
1588                         closest_target = tmp_entity;
1589                 tmp_entity = tmp_entity.chain;
1590         }
1591         
1592         return closest_target;
1593 }
1594
1595 /*
1596  * Find control point or generator owned by the same team self which is nearest to pos
1597  * if max_dist is positive, only control points within this range will be considered
1598  * This function only check distances on the XY plane, disregarding Z
1599  */
1600 entity ons_Nearest_ControlPoint_2D(vector pos, float max_dist)
1601 {
1602         entity tmp_entity, closest_target = world;
1603         vector delta;
1604         float smallest_distance = 0, distance;
1605         
1606         tmp_entity = findchain(classname, "onslaught_controlpoint");
1607         while(tmp_entity)
1608         {
1609                 delta = tmp_entity.origin - pos;
1610                 delta_z = 0;
1611                 distance = vlen(delta);
1612                 
1613                 if(SAME_TEAM(tmp_entity, self))
1614                 if(tmp_entity.iscaptured)
1615                 if(max_dist <= 0 || distance <= max_dist)
1616                 if(closest_target == world || distance <= smallest_distance )
1617                 {
1618                         closest_target = tmp_entity;
1619                         smallest_distance = distance;
1620                 }
1621                 
1622                 tmp_entity = tmp_entity.chain;
1623         }
1624         tmp_entity = findchain(classname, "onslaught_generator");
1625         while(tmp_entity)
1626         {
1627                 delta = tmp_entity.origin - pos;
1628                 delta_z = 0;
1629                 distance = vlen(delta);
1630                 
1631                 if(SAME_TEAM(tmp_entity, self))
1632                 if(max_dist <= 0 || distance <= max_dist)
1633                 if(closest_target == world || distance <= smallest_distance )
1634                 {
1635                         closest_target = tmp_entity;
1636                         smallest_distance = distance;
1637                 }
1638                 
1639                 tmp_entity = tmp_entity.chain;
1640         }
1641         
1642         return closest_target;
1643 }
1644 /**
1645  * find the number of control points and generators in the same team as self
1646  */
1647 int ons_Count_SelfControlPoints()
1648 {
1649         entity tmp_entity;
1650         tmp_entity = findchain(classname, "onslaught_controlpoint");
1651         int n = 0;
1652         while(tmp_entity)
1653         {
1654                 if(SAME_TEAM(tmp_entity, self))
1655                 if(tmp_entity.iscaptured)
1656                         n++;
1657                 tmp_entity = tmp_entity.chain;
1658         }
1659         tmp_entity = findchain(classname, "onslaught_generator");
1660         while(tmp_entity)
1661         {
1662                 if(SAME_TEAM(tmp_entity, self))
1663                         n++;
1664                 tmp_entity = tmp_entity.chain;
1665         }
1666         return n;
1667 }
1668
1669 /**
1670  * Teleport player to a random position near tele_target
1671  * if tele_effects is true, teleport sound+particles are created
1672  * return false on failure
1673  */
1674 bool ons_Teleport(entity player, entity tele_target, float range, bool tele_effects)
1675 {
1676         if ( !tele_target )
1677                 return false;
1678         
1679         int i;
1680         vector loc;
1681         float theta;
1682         for(i = 0; i < 16; ++i)
1683         {
1684                 theta = random() * 2 * M_PI;
1685                 loc_y = sin(theta);
1686                 loc_x = cos(theta);
1687                 loc_z = 0;
1688                 loc *= random() * range;
1689                 
1690                 loc += tele_target.origin + '0 0 128';
1691                 
1692                 tracebox(loc, PL_MIN, PL_MAX, loc, MOVE_NORMAL, player);
1693                 if(trace_fraction == 1.0 && !trace_startsolid)
1694                 {
1695                         traceline(tele_target.origin, loc, MOVE_NOMONSTERS, tele_target); // double check to make sure we're not spawning outside the world
1696                         if(trace_fraction == 1.0 && !trace_startsolid)
1697                         {
1698                                 if ( tele_effects )
1699                                 {
1700                                         pointparticles(particleeffectnum("teleport"), player.origin, '0 0 0', 1);
1701                                         sound (player, CH_TRIGGER, "misc/teleport.wav", VOL_BASE, ATTEN_NORM);
1702                                 }
1703                                 setorigin(player, loc);
1704                                 player.angles = '0 1 0' * ( theta * RAD2DEG + 180 );
1705                                 makevectors(player.angles);
1706                                 player.fixangle = true;
1707                                 player.teleport_antispam = time + autocvar_g_onslaught_teleport_wait;
1708
1709                                 if ( tele_effects )
1710                                         pointparticles(particleeffectnum("teleport"), player.origin + v_forward * 32, '0 0 0', 1);
1711                                 return true;
1712                         }
1713                 }
1714         }
1715         
1716         return false;
1717 }
1718
1719 // ==============
1720 // Hook Functions
1721 // ==============
1722
1723 MUTATOR_HOOKFUNCTION(ons_ResetMap)
1724 {
1725         FOR_EACH_PLAYER(self)
1726         {
1727                 self.ons_roundlost = false;
1728                 self.ons_deathloc = '0 0 0';
1729                 PutClientInServer();
1730         }
1731         return false;
1732 }
1733
1734 MUTATOR_HOOKFUNCTION(ons_RemovePlayer)
1735 {
1736         self.ons_deathloc = '0 0 0';
1737         return false;
1738 }
1739
1740 MUTATOR_HOOKFUNCTION(ons_PlayerSpawn)
1741 {
1742         if(!round_handler_IsRoundStarted())
1743         {
1744                 self.player_blocked = true;
1745                 return false;
1746         }
1747         
1748         entity l;
1749         for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
1750         {
1751                 l.sprite.SendFlags |= 16;
1752         }
1753         for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
1754         {
1755                 l.sprite.SendFlags |= 16;
1756         }
1757
1758         if(ons_stalemate) { Send_Notification(NOTIF_ONE, self, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT); }
1759
1760         if ( autocvar_g_onslaught_spawn_choose )
1761         if ( self.ons_spawn_by )
1762         if ( ons_Teleport(self,self.ons_spawn_by,autocvar_g_onslaught_teleport_radius,false) )
1763         {
1764                 self.ons_spawn_by = world;
1765                 return false;
1766         }
1767         
1768         if(autocvar_g_onslaught_spawn_at_controlpoints)
1769         if(random() <= autocvar_g_onslaught_spawn_at_controlpoints_chance)
1770         {
1771                 float random_target = autocvar_g_onslaught_spawn_at_controlpoints_random;
1772                 entity tmp_entity, closest_target = world;
1773                 vector spawn_loc = self.ons_deathloc;
1774                 
1775                 // new joining player or round reset, don't bother checking
1776                 if(spawn_loc == '0 0 0') { return false; }
1777
1778                 if(random_target) { RandomSelection_Init(); }
1779
1780                 for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext)
1781                 {
1782                         if(SAME_TEAM(tmp_entity, self))
1783                         if(random_target)
1784                                 RandomSelection_Add(tmp_entity, 0, string_null, 1, 1);
1785                         else if(vlen(tmp_entity.origin - spawn_loc) <= vlen(closest_target.origin - spawn_loc) || closest_target == world)
1786                                 closest_target = tmp_entity;
1787                 }
1788                 
1789                 if(random_target) { closest_target = RandomSelection_chosen_ent; }
1790                 
1791                 if(closest_target)
1792                 {
1793                         float i;
1794                         vector loc;
1795                         for(i = 0; i < 10; ++i)
1796                         {
1797                                 loc = closest_target.origin + '0 0 96';
1798                                 loc += ('0 1 0' * random()) * 128;
1799                                 tracebox(loc, PL_MIN, PL_MAX, loc, MOVE_NORMAL, self);
1800                                 if(trace_fraction == 1.0 && !trace_startsolid)
1801                                 {
1802                                         traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the world
1803                                         if(trace_fraction == 1.0 && !trace_startsolid)
1804                                         {
1805                                                 setorigin(self, loc);
1806                                                 self.angles = normalize(loc - closest_target.origin) * RAD2DEG;
1807                                                 return false;
1808                                         }
1809                                 }
1810                         }
1811                 }
1812         }
1813         
1814         if(autocvar_g_onslaught_spawn_at_generator)
1815         if(random() <= autocvar_g_onslaught_spawn_at_generator_chance)
1816         {
1817                 float random_target = autocvar_g_onslaught_spawn_at_generator_random;
1818                 entity tmp_entity, closest_target = world;
1819                 vector spawn_loc = self.ons_deathloc;
1820                 
1821                 // new joining player or round reset, don't bother checking
1822                 if(spawn_loc == '0 0 0') { return false; }
1823                 
1824                 if(random_target) { RandomSelection_Init(); }
1825
1826                 for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
1827                 {
1828                         if(random_target)
1829                                 RandomSelection_Add(tmp_entity, 0, string_null, 1, 1);
1830                         else
1831                         {
1832                                 if(SAME_TEAM(tmp_entity, self))
1833                                 if(vlen(tmp_entity.origin - spawn_loc) <= vlen(closest_target.origin - spawn_loc) || closest_target == world)
1834                                         closest_target = tmp_entity;
1835                         }
1836                 }
1837                 
1838                 if(random_target) { closest_target = RandomSelection_chosen_ent; }
1839                 
1840                 if(closest_target)
1841                 {
1842                         float i;
1843                         vector loc;
1844                         for(i = 0; i < 10; ++i)
1845                         {
1846                                 loc = closest_target.origin + '0 0 128';
1847                                 loc += ('0 1 0' * random()) * 256;
1848                                 tracebox(loc, PL_MIN, PL_MAX, loc, MOVE_NORMAL, self);
1849                                 if(trace_fraction == 1.0 && !trace_startsolid)
1850                                 {
1851                                         traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the world
1852                                         if(trace_fraction == 1.0 && !trace_startsolid)
1853                                         {
1854                                                 setorigin(self, loc);
1855                                                 self.angles = normalize(loc - closest_target.origin) * RAD2DEG;
1856                                                 return false;
1857                                         }
1858                                 }
1859                         }
1860                 }
1861         }
1862
1863     return false;
1864 }
1865
1866 MUTATOR_HOOKFUNCTION(ons_PlayerDies)
1867 {
1868         frag_target.ons_deathloc = frag_target.origin;
1869         entity l;
1870         for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
1871         {
1872                 l.sprite.SendFlags |= 16;
1873         }
1874         for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
1875         {
1876                 l.sprite.SendFlags |= 16;
1877         }
1878         
1879         if ( autocvar_g_onslaught_spawn_choose )
1880         if ( ons_Count_SelfControlPoints() > 1 )
1881                 stuffcmd(self, "qc_cmd_cl hud clickradar\n");
1882         
1883         return false;
1884 }
1885
1886 MUTATOR_HOOKFUNCTION(ons_MonsterThink)
1887 {
1888         entity e = find(world, targetname, self.target);
1889         if (e != world)
1890                 self.team = e.team;
1891
1892         return false;
1893 }
1894
1895 void ons_MonsterSpawn_Delayed()
1896 {
1897         entity e, own = self.owner;
1898         
1899         if(!own) { remove(self); return; }
1900         
1901         if(own.targetname)
1902         {
1903                 e = find(world, target, own.targetname);
1904                 if(e != world)
1905                 {
1906                         own.team = e.team;
1907                         
1908                         activator = e;
1909                         own.use();
1910                 }
1911         }
1912         
1913         remove(self);
1914 }
1915
1916 MUTATOR_HOOKFUNCTION(ons_MonsterSpawn)
1917 {
1918         entity e = spawn();
1919         e.owner = self;
1920         InitializeEntity(e, ons_MonsterSpawn_Delayed, INITPRIO_FINDTARGET);
1921
1922         return false;
1923 }
1924
1925 void ons_TurretSpawn_Delayed()
1926 {
1927         entity e, own = self.owner;
1928
1929         if(!own) { remove(self); return; }
1930
1931         if(own.targetname)
1932         {
1933                 e = find(world, target, own.targetname);
1934                 if(e != world)
1935                 {
1936                         own.team = e.team;
1937                         own.active = ACTIVE_NOT;
1938                         
1939                         activator = e;
1940                         own.use();
1941                 }
1942         }
1943
1944         remove(self);
1945 }
1946
1947 MUTATOR_HOOKFUNCTION(ons_TurretSpawn)
1948 {
1949         entity e = spawn();
1950         e.owner = self;
1951         InitializeEntity(e, ons_TurretSpawn_Delayed, INITPRIO_FINDTARGET);
1952
1953         return false;
1954 }
1955
1956 MUTATOR_HOOKFUNCTION(ons_BotRoles)
1957 {
1958         havocbot_ons_reset_role(self);
1959         return true;
1960 }
1961
1962 MUTATOR_HOOKFUNCTION(ons_GetTeamCount)
1963 {
1964         // onslaught is special
1965         entity tmp_entity;
1966         for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
1967         {
1968                 switch(tmp_entity.team)
1969                 {
1970                         case NUM_TEAM_1: c1 = 0; break;
1971                         case NUM_TEAM_2: c2 = 0; break;
1972                         case NUM_TEAM_3: c3 = 0; break;
1973                         case NUM_TEAM_4: c4 = 0; break;
1974                 }
1975         }
1976
1977         return true;
1978 }
1979
1980 MUTATOR_HOOKFUNCTION(ons_SpectateCopy)
1981 {
1982         self.ons_roundlost = other.ons_roundlost; // make spectators see it too
1983         return false;
1984 }
1985
1986 MUTATOR_HOOKFUNCTION(ons_SV_ParseClientCommand)
1987 {
1988         if(MUTATOR_RETURNVALUE) // command was already handled?
1989                 return false;
1990
1991         if ( cmd_name == "ons_spawn" )
1992         {
1993                 vector pos = self.origin;
1994                 if(cmd_argc > 1)
1995                         pos_x = stof(argv(1));
1996                 if(cmd_argc > 2)
1997                         pos_y = stof(argv(2));
1998                 if(cmd_argc > 3)
1999                         pos_z = stof(argv(3));
2000                 
2001                 if ( IS_PLAYER(self) )
2002                 {
2003                         if ( !self.frozen )
2004                         {
2005                                 entity source_point = ons_Nearest_ControlPoint(self.origin, autocvar_g_onslaught_teleport_radius);
2006                                 
2007                                 if ( !source_point && self.health > 0 )
2008                                 {
2009                                         sprint(self, "\nYou need to be next to a control point\n");
2010                                         return 1;
2011                                 }
2012                                 
2013                                 
2014                                 entity closest_target = ons_Nearest_ControlPoint_2D(pos, autocvar_g_onslaught_click_radius);
2015                         
2016                                 if ( closest_target == world )
2017                                 {
2018                                         sprint(self, "\nNo control point found\n");
2019                                         return 1;
2020                                 }
2021                                 
2022                                 if ( self.health <= 0 )
2023                                 {
2024                                         self.ons_spawn_by = closest_target;
2025                                         self.respawn_flags = self.respawn_flags | RESPAWN_FORCE;
2026                                 }
2027                                 else
2028                                 {
2029                                         if ( source_point == closest_target )
2030                                         {
2031                                                 sprint(self, "\nTeleporting to the same point\n");
2032                                                 return 1;
2033                                         }
2034                                         
2035                                         if ( !ons_Teleport(self,closest_target,autocvar_g_onslaught_teleport_radius,true) )
2036                                                 sprint(self, "\nUnable to teleport there\n");
2037                                 }
2038                                 
2039                                 return 1;
2040                         }
2041                         
2042                         sprint(self, "\nNo teleportation for you\n");
2043                 }
2044                 
2045                 return 1;
2046         }
2047         return 0;
2048 }
2049
2050 MUTATOR_HOOKFUNCTION(ons_PlayerUseKey)
2051 {
2052         if(MUTATOR_RETURNVALUE || gameover) { return false; }
2053         
2054         if((time > self.teleport_antispam) && (self.deadflag == DEAD_NO) && !self.vehicle)
2055         {
2056                 entity source_point = ons_Nearest_ControlPoint(self.origin, autocvar_g_onslaught_teleport_radius);
2057                 if ( source_point )
2058                 {
2059                         stuffcmd(self, "qc_cmd_cl hud clickradar\n");
2060                         return true;
2061                 }
2062         }
2063         
2064         return false;
2065 }
2066
2067 // ==========
2068 // Spawnfuncs
2069 // ==========
2070
2071 /*QUAKED spawnfunc_onslaught_link (0 .5 .8) (-16 -16 -16) (16 16 16)
2072   Link between control points.
2073
2074   This entity targets two different spawnfunc_onslaught_controlpoint or spawnfunc_onslaught_generator entities, and suppresses shielding on both if they are owned by different teams.
2075
2076 keys:
2077 "target" - first control point.
2078 "target2" - second control point.
2079  */
2080 void spawnfunc_onslaught_link()
2081 {
2082         if(!g_onslaught) { remove(self); return; }
2083
2084         if (self.target == "" || self.target2 == "")
2085                 objerror("target and target2 must be set\n");
2086
2087         self.ons_worldlinknext = ons_worldlinklist; // link into ons_worldlinklist
2088         ons_worldlinklist = self;
2089
2090         InitializeEntity(self, ons_DelayedLinkSetup, INITPRIO_FINDTARGET);
2091         Net_LinkEntity(self, false, 0, ons_Link_Send);
2092 }
2093
2094 /*QUAKED spawnfunc_onslaught_controlpoint (0 .5 .8) (-32 -32 0) (32 32 128)
2095   Control point. Be sure to give this enough clearance so that the shootable part has room to exist
2096
2097   This should link to an spawnfunc_onslaught_controlpoint entity or spawnfunc_onslaught_generator entity.
2098
2099 keys:
2100 "targetname" - name that spawnfunc_onslaught_link entities will use to target this.
2101 "target" - target any entities that are tied to this control point, such as vehicles and buildable structure entities.
2102 "message" - name of this control point (should reflect the location in the map, such as "center bridge", "north tower", etc)
2103  */
2104
2105 void spawnfunc_onslaught_controlpoint()
2106 {
2107         if(!g_onslaught) { remove(self); return; }
2108         
2109         ons_ControlPoint_Setup(self);
2110 }
2111
2112 /*QUAKED spawnfunc_onslaught_generator (0 .5 .8) (-32 -32 -24) (32 32 64)
2113   Base generator.
2114
2115   spawnfunc_onslaught_link entities can target this.
2116
2117 keys:
2118 "team" - team that owns this generator (5 = red, 14 = blue, etc), MUST BE SET.
2119 "targetname" - name that spawnfunc_onslaught_link entities will use to target this.
2120  */
2121 void spawnfunc_onslaught_generator()
2122 {
2123         if(!g_onslaught) { remove(self); return; }
2124         if(!self.team) { objerror("team must be set"); }
2125
2126         ons_GeneratorSetup(self);
2127 }
2128
2129
2130 // scoreboard setup
2131 void ons_ScoreRules()
2132 {
2133         CheckAllowedTeams(world);
2134         ScoreRules_basics(((c4>=0) ? 4 : (c3>=0) ? 3 : 2), SFL_SORT_PRIO_PRIMARY, 0, true);
2135         ScoreInfo_SetLabel_TeamScore  (ST_ONS_CAPS,     "destroyed", SFL_SORT_PRIO_PRIMARY);
2136         ScoreInfo_SetLabel_PlayerScore(SP_ONS_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2137         ScoreInfo_SetLabel_PlayerScore(SP_ONS_TAKES,    "takes",     0);
2138         ScoreRules_basics_end();
2139 }
2140
2141 void ons_DelayedInit() // Do this check with a delay so we can wait for teams to be set up
2142 {
2143         ons_ScoreRules();
2144         
2145         round_handler_Spawn(Onslaught_CheckPlayers, Onslaught_CheckWinner, Onslaught_RoundStart);
2146         round_handler_Init(5, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit);
2147 }
2148
2149 void ons_Initialize()
2150 {
2151         precache_sound("ctf/red_capture.wav");
2152         precache_sound("ctf/blue_capture.wav");
2153         precache_sound("ctf/yellow_capture.wav");
2154         precache_sound("ctf/pink_capture.wav");
2155
2156         ons_captureshield_force = autocvar_g_onslaught_shield_force;
2157
2158         addstat(STAT_ROUNDLOST, AS_INT, ons_roundlost);
2159
2160         InitializeEntity(world, ons_DelayedInit, INITPRIO_GAMETYPE);
2161 }
2162
2163 MUTATOR_DEFINITION(gamemode_onslaught)
2164 {
2165         MUTATOR_HOOK(reset_map_global, ons_ResetMap, CBC_ORDER_ANY);
2166         MUTATOR_HOOK(MakePlayerObserver, ons_RemovePlayer, CBC_ORDER_ANY);
2167         MUTATOR_HOOK(ClientDisconnect, ons_RemovePlayer, CBC_ORDER_ANY);
2168         MUTATOR_HOOK(PlayerSpawn, ons_PlayerSpawn, CBC_ORDER_ANY);
2169         MUTATOR_HOOK(PlayerDies, ons_PlayerDies, CBC_ORDER_ANY);
2170         MUTATOR_HOOK(MonsterMove, ons_MonsterThink, CBC_ORDER_ANY);
2171         MUTATOR_HOOK(MonsterSpawn, ons_MonsterSpawn, CBC_ORDER_ANY);
2172         MUTATOR_HOOK(TurretSpawn, ons_TurretSpawn, CBC_ORDER_ANY);
2173         MUTATOR_HOOK(HavocBot_ChooseRole, ons_BotRoles, CBC_ORDER_ANY);
2174         MUTATOR_HOOK(GetTeamCount, ons_GetTeamCount, CBC_ORDER_ANY);
2175         MUTATOR_HOOK(SpectateCopy, ons_SpectateCopy, CBC_ORDER_ANY);
2176         MUTATOR_HOOK(SV_ParseClientCommand, ons_SV_ParseClientCommand, CBC_ORDER_ANY);
2177         MUTATOR_HOOK(PlayerUseKey, ons_PlayerUseKey, CBC_ORDER_ANY);
2178
2179         MUTATOR_ONADD
2180         {
2181                 if(time > 1) // game loads at time 1
2182                         error("This is a game type and it cannot be added at runtime.");
2183                 ons_Initialize();
2184         }
2185
2186         MUTATOR_ONROLLBACK_OR_REMOVE
2187         {
2188                 // we actually cannot roll back ons_Initialize here
2189                 // BUT: we don't need to! If this gets called, adding always
2190                 // succeeds.
2191         }
2192
2193         MUTATOR_ONREMOVE
2194         {
2195                 print("This is a game type and it cannot be removed at runtime.");
2196                 return -1;
2197         }
2198
2199         return false;
2200 }