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