]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/gamemodes/gamemode/onslaught/onslaught.qc
Merge branch 'master' into terencehill/menu_hudskin_selector
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / gamemodes / gamemode / onslaught / 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         MUTATOR_ONADD
10         {
11                 if (time > 1) // game loads at time 1
12                         error("This is a game type and it cannot be added at runtime.");
13                 ons_Initialize();
14
15                 ActivateTeamplay();
16                 SetLimits(autocvar_g_onslaught_point_limit, -1, -1, -1);
17                 have_team_spawns = -1; // request team spawns
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 "sv_controlpoint.qh"
132 #include "sv_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(entity this)
199 {
200         this.colormap = this.enemy.colormap;
201         this.team = this.enemy.team;
202 }
203
204 void ons_CaptureShield_Spawn(entity generator, bool is_generator)
205 {
206         entity shield = new(ons_captureshield);
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.effects = EF_ADDITIVE;
215         shield.movetype = MOVETYPE_NOCLIP;
216         shield.solid = SOLID_TRIGGER;
217         shield.avelocity = '7 0 11';
218         shield.scale = 1;
219         shield.model = ((is_generator) ? "models/onslaught/generator_shield.md3" : "models/onslaught/controlpoint_shield.md3");
220
221         precache_model(shield.model);
222         setorigin(shield, generator.origin);
223         _setmodel(shield, shield.model);
224         setsize(shield, shield.scale * shield.mins, shield.scale * shield.maxs);
225 }
226
227
228 // ==========
229 // Junk Pile
230 // ==========
231
232 void ons_debug(string input)
233 {
234         switch(autocvar_g_onslaught_debug)
235         {
236                 case 1: LOG_TRACE(input); break;
237                 case 2: LOG_INFO(input); break;
238         }
239 }
240
241 void setmodel_fixsize(entity e, Model m)
242 {
243         setmodel(e, m);
244         FixSize(e);
245 }
246
247 void onslaught_updatelinks()
248 {
249         entity l;
250         // first check if the game has ended
251         ons_debug("--- updatelinks ---\n");
252         // mark generators as being shielded and networked
253         for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
254         {
255                 if (l.iscaptured)
256                         ons_debug(strcat(etos(l), " (generator) belongs to team ", ftos(l.team), "\n"));
257                 else
258                         ons_debug(strcat(etos(l), " (generator) is destroyed\n"));
259                 l.islinked = l.iscaptured;
260                 l.isshielded = l.iscaptured;
261                 l.sprite.SendFlags |= 16;
262         }
263         // mark points as shielded and not networked
264         for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
265         {
266                 l.islinked = false;
267                 l.isshielded = true;
268                 int i;
269                 for(i = 0; i < 17; ++i) { l.isgenneighbor[i] = false; l.iscpneighbor[i] = false; }
270                 ons_debug(strcat(etos(l), " (point) belongs to team ", ftos(l.team), "\n"));
271                 l.sprite.SendFlags |= 16;
272         }
273         // flow power outward from the generators through the network
274         bool stop = false;
275         while (!stop)
276         {
277                 stop = true;
278                 for(l = ons_worldlinklist; l; l = l.ons_worldlinknext)
279                 {
280                         // if both points are captured by the same team, and only one of
281                         // them is powered, mark the other one as powered as well
282                         if (l.enemy.iscaptured && l.goalentity.iscaptured)
283                                 if (l.enemy.islinked != l.goalentity.islinked)
284                                         if(SAME_TEAM(l.enemy, l.goalentity))
285                                         {
286                                                 if (!l.goalentity.islinked)
287                                                 {
288                                                         stop = false;
289                                                         l.goalentity.islinked = true;
290                                                         ons_debug(strcat(etos(l), " (link) is marking ", etos(l.goalentity), " (point) because its team matches ", etos(l.enemy), " (point)\n"));
291                                                 }
292                                                 else if (!l.enemy.islinked)
293                                                 {
294                                                         stop = false;
295                                                         l.enemy.islinked = true;
296                                                         ons_debug(strcat(etos(l), " (link) is marking ", etos(l.enemy), " (point) because its team matches ", etos(l.goalentity), " (point)\n"));
297                                                 }
298                                         }
299                 }
300         }
301         // now that we know which points are powered we can mark their neighbors
302         // as unshielded if team differs
303         for(l = ons_worldlinklist; l; l = l.ons_worldlinknext)
304         {
305                 if (l.goalentity.islinked)
306                 {
307                         if(DIFF_TEAM(l.goalentity, l.enemy))
308                         {
309                                 ons_debug(strcat(etos(l), " (link) is unshielding ", etos(l.enemy), " (point) because its team does not match ", etos(l.goalentity), " (point)\n"));
310                                 l.enemy.isshielded = false;
311                         }
312                         if(l.goalentity.classname == "onslaught_generator")
313                                 l.enemy.isgenneighbor[l.goalentity.team] = true;
314                         else
315                                 l.enemy.iscpneighbor[l.goalentity.team] = true;
316                 }
317                 if (l.enemy.islinked)
318                 {
319                         if(DIFF_TEAM(l.goalentity, l.enemy))
320                         {
321                                 ons_debug(strcat(etos(l), " (link) is unshielding ", etos(l.goalentity), " (point) because its team does not match ", etos(l.enemy), " (point)\n"));
322                                 l.goalentity.isshielded = false;
323                         }
324                         if(l.enemy.classname == "onslaught_generator")
325                                 l.goalentity.isgenneighbor[l.enemy.team] = true;
326                         else
327                                 l.goalentity.iscpneighbor[l.enemy.team] = true;
328                 }
329         }
330         // now update the generators
331         for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
332         {
333                 if (l.isshielded)
334                 {
335                         ons_debug(strcat(etos(l), " (generator) is shielded\n"));
336                         l.takedamage = DAMAGE_NO;
337                         l.bot_attack = false;
338                 }
339                 else
340                 {
341                         ons_debug(strcat(etos(l), " (generator) is not shielded\n"));
342                         l.takedamage = DAMAGE_AIM;
343                         l.bot_attack = true;
344                 }
345
346                 ons_Generator_UpdateSprite(l);
347         }
348         // now update the takedamage and alpha variables on control point icons
349         for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
350         {
351                 if (l.isshielded)
352                 {
353                         ons_debug(strcat(etos(l), " (point) is shielded\n"));
354                         if (l.goalentity)
355                         {
356                                 l.goalentity.takedamage = DAMAGE_NO;
357                                 l.goalentity.bot_attack = false;
358                         }
359                 }
360                 else
361                 {
362                         ons_debug(strcat(etos(l), " (point) is not shielded\n"));
363                         if (l.goalentity)
364                         {
365                                 l.goalentity.takedamage = DAMAGE_AIM;
366                                 l.goalentity.bot_attack = true;
367                         }
368                 }
369                 ons_ControlPoint_UpdateSprite(l);
370         }
371         l = findchain(classname, "ons_captureshield");
372         while(l)
373         {
374                 l.team = l.enemy.team;
375                 l.colormap = l.enemy.colormap;
376                 l = l.chain;
377         }
378 }
379
380
381 // ===================
382 // Main Link Functions
383 // ===================
384
385 bool ons_Link_Send(entity this, entity to, int sendflags)
386 {
387         WriteHeader(MSG_ENTITY, ENT_CLIENT_RADARLINK);
388         WriteByte(MSG_ENTITY, sendflags);
389         if(sendflags & 1)
390         {
391                 WriteCoord(MSG_ENTITY, self.goalentity.origin_x);
392                 WriteCoord(MSG_ENTITY, self.goalentity.origin_y);
393                 WriteCoord(MSG_ENTITY, self.goalentity.origin_z);
394         }
395         if(sendflags & 2)
396         {
397                 WriteCoord(MSG_ENTITY, self.enemy.origin_x);
398                 WriteCoord(MSG_ENTITY, self.enemy.origin_y);
399                 WriteCoord(MSG_ENTITY, self.enemy.origin_z);
400         }
401         if(sendflags & 4)
402         {
403                 WriteByte(MSG_ENTITY, self.clientcolors); // which is goalentity's color + enemy's color * 16
404         }
405         return true;
406 }
407
408 void ons_Link_CheckUpdate()
409 {SELFPARAM();
410         // TODO check if the two sides have moved (currently they won't move anyway)
411         float cc = 0, cc1 = 0, cc2 = 0;
412
413         if(self.goalentity.islinked || self.goalentity.iscaptured) { cc1 = (self.goalentity.team - 1) * 0x01; }
414         if(self.enemy.islinked || self.enemy.iscaptured) { cc2 = (self.enemy.team - 1) * 0x10; }
415
416         cc = cc1 + cc2;
417
418         if(cc != self.clientcolors)
419         {
420                 self.clientcolors = cc;
421                 self.SendFlags |= 4;
422         }
423
424         self.nextthink = time;
425 }
426
427 void ons_DelayedLinkSetup()
428 {SELFPARAM();
429         self.goalentity = find(world, targetname, self.target);
430         self.enemy = find(world, targetname, self.target2);
431         if(!self.goalentity) { objerror("can not find target\n"); }
432         if(!self.enemy) { objerror("can not find target2\n"); }
433
434         ons_debug(strcat(etos(self.goalentity), " linked with ", etos(self.enemy), "\n"));
435         self.SendFlags |= 3;
436         self.think = ons_Link_CheckUpdate;
437         self.nextthink = time;
438 }
439
440
441 // =============================
442 // Main Control Point Functions
443 // =============================
444
445 int ons_ControlPoint_CanBeLinked(entity cp, int teamnumber)
446 {
447         if(cp.isgenneighbor[teamnumber]) { return 2; }
448         if(cp.iscpneighbor[teamnumber]) { return 1; }
449
450         return 0;
451 }
452
453 int ons_ControlPoint_Attackable(entity cp, int teamnumber)
454         // -2: SAME TEAM, attackable by enemy!
455         // -1: SAME TEAM!
456         // 0: off limits
457         // 1: attack it
458         // 2: touch it
459         // 3: attack it (HIGH PRIO)
460         // 4: touch it (HIGH PRIO)
461 {
462         int a;
463
464         if(cp.isshielded)
465         {
466                 return 0;
467         }
468         else if(cp.goalentity)
469         {
470                 // if there's already an icon built, nothing happens
471                 if(cp.team == teamnumber)
472                 {
473                         a = ons_ControlPoint_CanBeLinked(cp, teamnumber);
474                         if(a) // attackable by enemy?
475                                 return -2; // EMERGENCY!
476                         return -1;
477                 }
478                 // we know it can be linked, so no need to check
479                 // but...
480                 a = ons_ControlPoint_CanBeLinked(cp, teamnumber);
481                 if(a == 2) // near our generator?
482                         return 3; // EMERGENCY!
483                 return 1;
484         }
485         else
486         {
487                 // free point
488                 if(ons_ControlPoint_CanBeLinked(cp, teamnumber))
489                 {
490                         a = ons_ControlPoint_CanBeLinked(cp, teamnumber); // why was this here NUM_TEAM_1 + NUM_TEAM_2 - t
491                         if(a == 2)
492                                 return 4; // GET THIS ONE NOW!
493                         else
494                                 return 2; // TOUCH ME
495                 }
496         }
497         return 0;
498 }
499
500 void ons_ControlPoint_Icon_Damage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
501 {SELFPARAM();
502         if(damage <= 0) { return; }
503
504         if (self.owner.isshielded)
505         {
506                 // this is protected by a shield, so ignore the damage
507                 if (time > self.pain_finished)
508                         if (IS_PLAYER(attacker))
509                         {
510                                 play2(attacker, SND(ONS_DAMAGEBLOCKEDBYSHIELD));
511                                 self.pain_finished = time + 1;
512                                 attacker.typehitsound += 1; // play both sounds (shield is way too quiet)
513                         }
514
515                 return;
516         }
517
518         if(IS_PLAYER(attacker))
519         if(time - ons_notification_time[self.team] > 10)
520         {
521                 play2team(self.team, SND(ONS_CONTROLPOINT_UNDERATTACK));
522                 ons_notification_time[self.team] = time;
523         }
524
525         self.health = self.health - damage;
526         if(self.owner.iscaptured)
527                 WaypointSprite_UpdateHealth(self.owner.sprite, self.health);
528         else
529                 WaypointSprite_UpdateBuildFinished(self.owner.sprite, time + (self.max_health - self.health) / (self.count / ONS_CP_THINKRATE));
530         self.pain_finished = time + 1;
531         // particles on every hit
532         pointparticles(EFFECT_SPARKS, hitloc, force*-1, 1);
533         //sound on every hit
534         if (random() < 0.5)
535                 sound(self, CH_TRIGGER, SND_ONS_HIT1, VOL_BASE+0.3, ATTEN_NORM);
536         else
537                 sound(self, CH_TRIGGER, SND_ONS_HIT2, VOL_BASE+0.3, ATTEN_NORM);
538
539         if (self.health < 0)
540         {
541                 sound(self, CH_TRIGGER, SND_GRENADE_IMPACT, VOL_BASE, ATTEN_NORM);
542                 pointparticles(EFFECT_ROCKET_EXPLODE, self.origin, '0 0 0', 1);
543                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(self.team, INFO_ONSLAUGHT_CPDESTROYED_), self.owner.message, attacker.netname);
544
545                 PlayerScore_Add(attacker, SP_ONS_TAKES, 1);
546                 PlayerScore_Add(attacker, SP_SCORE, 10);
547
548                 self.owner.goalentity = world;
549                 self.owner.islinked = false;
550                 self.owner.iscaptured = false;
551                 self.owner.team = 0;
552                 self.owner.colormap = 1024;
553
554                 WaypointSprite_UpdateMaxHealth(self.owner.sprite, 0);
555
556                 onslaught_updatelinks();
557
558                 // Use targets now (somebody make sure this is in the right place..)
559                 setself(self.owner);
560                 activator = self;
561                 SUB_UseTargets ();
562                 setself(this);
563
564                 self.owner.waslinked = self.owner.islinked;
565                 if(self.owner.model != "models/onslaught/controlpoint_pad.md3")
566                         setmodel_fixsize(self.owner, MDL_ONS_CP_PAD1);
567                 //setsize(self, '-32 -32 0', '32 32 8');
568
569                 remove(self);
570         }
571
572         self.SendFlags |= CPSF_STATUS;
573 }
574
575 void ons_ControlPoint_Icon_Think()
576 {SELFPARAM();
577         self.nextthink = time + ONS_CP_THINKRATE;
578
579         if(autocvar_g_onslaught_cp_proxydecap)
580         {
581         int _enemy_count = 0;
582         int _friendly_count = 0;
583         float _dist;
584         entity _player;
585
586         FOR_EACH_PLAYER(_player)
587         {
588             if(!_player.deadflag)
589             {
590                 _dist = vlen(_player.origin - self.origin);
591                 if(_dist < autocvar_g_onslaught_cp_proxydecap_distance)
592                 {
593                                         if(SAME_TEAM(_player, self))
594                         ++_friendly_count;
595                     else
596                         ++_enemy_count;
597                 }
598             }
599         }
600
601         _friendly_count = _friendly_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE);
602         _enemy_count = _enemy_count * (autocvar_g_onslaught_cp_proxydecap_dps * ONS_CP_THINKRATE);
603
604         self.health = bound(0, self.health + (_friendly_count - _enemy_count), self.max_health);
605                 self.SendFlags |= CPSF_STATUS;
606         if(self.health <= 0)
607         {
608             ons_ControlPoint_Icon_Damage(self, self, 1, 0, self.origin, '0 0 0');
609             return;
610         }
611     }
612
613         if (time > self.pain_finished + 5)
614         {
615                 if(self.health < self.max_health)
616                 {
617                         self.health = self.health + self.count;
618                         if (self.health >= self.max_health)
619                                 self.health = self.max_health;
620                         WaypointSprite_UpdateHealth(self.owner.sprite, self.health);
621                 }
622         }
623
624         if(self.owner.islinked != self.owner.waslinked)
625         {
626                 // unteam the spawnpoint if needed
627                 int t = self.owner.team;
628                 if(!self.owner.islinked)
629                         self.owner.team = 0;
630
631                 setself(self.owner);
632                 activator = self;
633                 SUB_UseTargets ();
634                 setself(this);
635
636                 self.owner.team = t;
637
638                 self.owner.waslinked = self.owner.islinked;
639         }
640
641         // damaged fx
642         if(random() < 0.6 - self.health / self.max_health)
643         {
644                 Send_Effect(EFFECT_ELECTRIC_SPARKS, self.origin + randompos('-10 -10 -20', '10 10 20'), '0 0 0', 1);
645
646                 if(random() > 0.8)
647                         sound(self, CH_PAIN, SND_ONS_SPARK1, VOL_BASE, ATTEN_NORM);
648                 else if (random() > 0.5)
649                         sound(self, CH_PAIN, SND_ONS_SPARK2, VOL_BASE, ATTEN_NORM);
650         }
651 }
652
653 void ons_ControlPoint_Icon_BuildThink()
654 {SELFPARAM();
655         int a;
656
657         self.nextthink = time + ONS_CP_THINKRATE;
658
659         // only do this if there is power
660         a = ons_ControlPoint_CanBeLinked(self.owner, self.owner.team);
661         if(!a)
662                 return;
663
664         self.health = self.health + self.count;
665
666         self.SendFlags |= CPSF_STATUS;
667
668         if (self.health >= self.max_health)
669         {
670                 self.health = self.max_health;
671                 self.count = autocvar_g_onslaught_cp_regen * ONS_CP_THINKRATE; // slow repair rate from now on
672                 self.think = ons_ControlPoint_Icon_Think;
673                 sound(self, CH_TRIGGER, SND_ONS_CONTROLPOINT_BUILT, VOL_BASE, ATTEN_NORM);
674                 self.owner.iscaptured = true;
675                 self.solid = SOLID_BBOX;
676
677                 Send_Effect(EFFECT_CAP(self.owner.team), self.owner.origin, '0 0 0', 1);
678
679                 WaypointSprite_UpdateMaxHealth(self.owner.sprite, self.max_health);
680                 WaypointSprite_UpdateHealth(self.owner.sprite, self.health);
681
682                 if(IS_PLAYER(self.owner.ons_toucher))
683                 {
684                         Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ONSLAUGHT_CAPTURE, self.owner.ons_toucher.netname, self.owner.message);
685                         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);
686                         Send_Notification(NOTIF_ONE, self.owner.ons_toucher, MSG_CENTER, CENTER_ONS_CAPTURE, self.owner.message);
687                         PlayerScore_Add(self.owner.ons_toucher, SP_ONS_CAPS, 1);
688                         PlayerTeamScore_AddScore(self.owner.ons_toucher, 10);
689                 }
690
691                 self.owner.ons_toucher = world;
692
693                 onslaught_updatelinks();
694
695                 // Use targets now (somebody make sure this is in the right place..)
696                 setself(self.owner);
697                 activator = self;
698                 SUB_UseTargets ();
699                 setself(this);
700
701                 self.SendFlags |= CPSF_SETUP;
702         }
703         if(self.owner.model != MDL_ONS_CP_PAD2.model_str())
704                 setmodel_fixsize(self.owner, MDL_ONS_CP_PAD2);
705
706         if(random() < 0.9 - self.health / self.max_health)
707                 Send_Effect(EFFECT_RAGE, self.origin + 10 * randomvec(), '0 0 -1', 1);
708 }
709
710 void onslaught_controlpoint_icon_link(entity e, void() spawnproc);
711
712 void ons_ControlPoint_Icon_Spawn(entity cp, entity player)
713 {
714         entity e = new(onslaught_controlpoint_icon);
715
716         setsize(e, CPICON_MIN, CPICON_MAX);
717         setorigin(e, cp.origin + CPICON_OFFSET);
718
719         e.owner = cp;
720         e.max_health = autocvar_g_onslaught_cp_health;
721         e.health = autocvar_g_onslaught_cp_buildhealth;
722         e.solid = SOLID_NOT;
723         e.takedamage = DAMAGE_AIM;
724         e.bot_attack = true;
725         e.event_damage = ons_ControlPoint_Icon_Damage;
726         e.team = player.team;
727         e.colormap = 1024 + (e.team - 1) * 17;
728         e.count = (e.max_health - e.health) * ONS_CP_THINKRATE / autocvar_g_onslaught_cp_buildtime; // how long it takes to build
729
730         sound(e, CH_TRIGGER, SND_ONS_CONTROLPOINT_BUILD, VOL_BASE, ATTEN_NORM);
731
732         cp.goalentity = e;
733         cp.team = e.team;
734         cp.colormap = e.colormap;
735
736         Send_Effect(EFFECT_FLAG_TOUCH(player.team), e.origin, '0 0 0', 1);
737
738         WaypointSprite_UpdateBuildFinished(cp.sprite, time + (e.max_health - e.health) / (e.count / ONS_CP_THINKRATE));
739         WaypointSprite_UpdateRule(cp.sprite,cp.team,SPRITERULE_TEAMPLAY);
740         cp.sprite.SendFlags |= 16;
741
742         onslaught_controlpoint_icon_link(e, ons_ControlPoint_Icon_BuildThink);
743 }
744
745 entity ons_ControlPoint_Waypoint(entity e)
746 {
747         if(e.team)
748         {
749                 int a = ons_ControlPoint_Attackable(e, e.team);
750
751                 if(a == -2) { return WP_OnsCPDefend; } // defend now
752                 if(a == -1 || a == 1 || a == 2) { return WP_OnsCP; } // touch
753                 if(a == 3 || a == 4) { return WP_OnsCPAttack; } // attack
754         }
755         else
756                 return WP_OnsCP;
757
758         return WP_Null;
759 }
760
761 void ons_ControlPoint_UpdateSprite(entity e)
762 {
763         entity s1 = ons_ControlPoint_Waypoint(e);
764         WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1);
765
766         bool sh;
767         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));
768
769         if(e.lastteam != e.team + 2 || e.lastshielded != sh || e.iscaptured != e.lastcaptured)
770         {
771                 if(e.iscaptured) // don't mess up build bars!
772                 {
773                         if(sh)
774                         {
775                                 WaypointSprite_UpdateMaxHealth(e.sprite, 0);
776                         }
777                         else
778                         {
779                                 WaypointSprite_UpdateMaxHealth(e.sprite, e.goalentity.max_health);
780                                 WaypointSprite_UpdateHealth(e.sprite, e.goalentity.health);
781                         }
782                 }
783                 if(e.lastshielded)
784                 {
785                         if(e.team)
786                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, 0.5 * colormapPaletteColor(e.team - 1, false));
787                         else
788                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.5 0.5 0.5');
789                 }
790                 else
791                 {
792                         if(e.team)
793                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, colormapPaletteColor(e.team - 1, false));
794                         else
795                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_CONTROLPOINT, '0.75 0.75 0.75');
796                 }
797                 WaypointSprite_Ping(e.sprite);
798
799                 e.lastteam = e.team + 2;
800                 e.lastshielded = sh;
801                 e.lastcaptured = e.iscaptured;
802         }
803 }
804
805 void ons_ControlPoint_Touch()
806 {SELFPARAM();
807         entity toucher = other;
808         int attackable;
809
810         if(IS_VEHICLE(toucher) && toucher.owner)
811         if(autocvar_g_onslaught_allow_vehicle_touch)
812                 toucher = toucher.owner;
813         else
814                 return;
815
816         if(!IS_PLAYER(toucher)) { return; }
817         if(toucher.frozen) { return; }
818         if(toucher.deadflag != DEAD_NO) { return; }
819
820         if ( SAME_TEAM(self,toucher) )
821         if ( self.iscaptured )
822         {
823                 if(time <= toucher.teleport_antispam)
824                         Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT_ANTISPAM, rint(toucher.teleport_antispam - time));
825                 else
826                         Send_Notification(NOTIF_ONE, toucher, MSG_CENTER, CENTER_ONS_TELEPORT);
827         }
828
829         attackable = ons_ControlPoint_Attackable(self, toucher.team);
830         if(attackable != 2 && attackable != 4)
831                 return;
832         // we've verified that this player has a legitimate claim to this point,
833         // so start building the captured point icon (which only captures this
834         // point if it successfully builds without being destroyed first)
835         ons_ControlPoint_Icon_Spawn(self, toucher);
836
837         self.ons_toucher = toucher;
838
839         onslaught_updatelinks();
840 }
841
842 void ons_ControlPoint_Think()
843 {SELFPARAM();
844         self.nextthink = time + ONS_CP_THINKRATE;
845         CSQCMODEL_AUTOUPDATE(self);
846 }
847
848 void ons_ControlPoint_Reset(entity this)
849 {
850         if(this.goalentity)
851                 remove(this.goalentity);
852
853         this.goalentity = world;
854         this.team = 0;
855         this.colormap = 1024;
856         this.iscaptured = false;
857         this.islinked = false;
858         this.isshielded = true;
859         this.think = ons_ControlPoint_Think;
860         this.ons_toucher = world;
861         this.nextthink = time + ONS_CP_THINKRATE;
862         setmodel_fixsize(this, MDL_ONS_CP_PAD1);
863
864         WaypointSprite_UpdateMaxHealth(this.sprite, 0);
865         WaypointSprite_UpdateRule(this.sprite,this.team,SPRITERULE_TEAMPLAY);
866
867         onslaught_updatelinks();
868
869         activator = this;
870         SUB_UseTargets(); // to reset the structures, playerspawns etc.
871
872         CSQCMODEL_AUTOUPDATE(this);
873 }
874
875 void ons_DelayedControlPoint_Setup()
876 {SELFPARAM();
877         onslaught_updatelinks();
878
879         // captureshield setup
880         ons_CaptureShield_Spawn(self, false);
881
882         CSQCMODEL_AUTOINIT(self);
883 }
884
885 void ons_ControlPoint_Setup(entity cp)
886 {SELFPARAM();
887         // declarations
888         setself(cp); // for later usage with droptofloor()
889
890         // main setup
891         cp.ons_worldcpnext = ons_worldcplist; // link control point into ons_worldcplist
892         ons_worldcplist = cp;
893
894         cp.netname = "Control point";
895         cp.team = 0;
896         cp.solid = SOLID_BBOX;
897         cp.movetype = MOVETYPE_NONE;
898         cp.touch = ons_ControlPoint_Touch;
899         cp.think = ons_ControlPoint_Think;
900         cp.nextthink = time + ONS_CP_THINKRATE;
901         cp.reset = ons_ControlPoint_Reset;
902         cp.colormap = 1024;
903         cp.iscaptured = false;
904         cp.islinked = false;
905         cp.isshielded = true;
906
907         if(cp.message == "") { cp.message = "a"; }
908
909         // appearence
910         setmodel_fixsize(cp, MDL_ONS_CP_PAD1);
911
912         // control point placement
913         if((cp.spawnflags & 1) || cp.noalign) // don't drop to floor, just stay at fixed location
914         {
915                 cp.noalign = true;
916                 cp.movetype = MOVETYPE_NONE;
917         }
918         else // drop to floor, automatically find a platform and set that as spawn origin
919         {
920                 setorigin(cp, cp.origin + '0 0 20');
921                 cp.noalign = false;
922                 setself(cp);
923                 droptofloor();
924                 cp.movetype = MOVETYPE_TOSS;
925         }
926
927         // waypointsprites
928         WaypointSprite_SpawnFixed(WP_Null, self.origin + CPGEN_WAYPOINT_OFFSET, self, sprite, RADARICON_NONE);
929         WaypointSprite_UpdateRule(self.sprite, self.team, SPRITERULE_TEAMPLAY);
930
931         InitializeEntity(cp, ons_DelayedControlPoint_Setup, INITPRIO_SETLOCATION);
932 }
933
934
935 // =========================
936 // Main Generator Functions
937 // =========================
938
939 entity ons_Generator_Waypoint(entity e)
940 {
941         if (e.isshielded)
942                 return WP_OnsGenShielded;
943         return WP_OnsGen;
944 }
945
946 void ons_Generator_UpdateSprite(entity e)
947 {
948         entity s1 = ons_Generator_Waypoint(e);
949         WaypointSprite_UpdateSprites(e.sprite, s1, s1, s1);
950
951         if(e.lastteam != e.team + 2 || e.lastshielded != e.isshielded)
952         {
953                 e.lastteam = e.team + 2;
954                 e.lastshielded = e.isshielded;
955                 if(e.lastshielded)
956                 {
957                         if(e.team)
958                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, 0.5 * colormapPaletteColor(e.team - 1, false));
959                         else
960                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.5 0.5 0.5');
961                 }
962                 else
963                 {
964                         if(e.team)
965                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, colormapPaletteColor(e.team - 1, false));
966                         else
967                                 WaypointSprite_UpdateTeamRadar(e.sprite, RADARICON_GENERATOR, '0.75 0.75 0.75');
968                 }
969                 WaypointSprite_Ping(e.sprite);
970         }
971 }
972
973 void ons_GeneratorDamage(entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force)
974 {SELFPARAM();
975         if(damage <= 0) { return; }
976         if(warmup_stage || gameover) { return; }
977         if(!round_handler_IsRoundStarted()) { return; }
978
979         if (attacker != self)
980         {
981                 if (self.isshielded)
982                 {
983                         // this is protected by a shield, so ignore the damage
984                         if (time > self.pain_finished)
985                                 if (IS_PLAYER(attacker))
986                                 {
987                                         play2(attacker, SND(ONS_DAMAGEBLOCKEDBYSHIELD));
988                                         attacker.typehitsound += 1;
989                                         self.pain_finished = time + 1;
990                                 }
991                         return;
992                 }
993                 if (time > self.pain_finished)
994                 {
995                         self.pain_finished = time + 10;
996                         entity head;
997                         FOR_EACH_REALPLAYER(head) if(SAME_TEAM(head, self)) { Send_Notification(NOTIF_ONE, head, MSG_CENTER, CENTER_GENERATOR_UNDERATTACK); }
998                         play2team(self.team, SND(ONS_GENERATOR_UNDERATTACK));
999                 }
1000         }
1001         self.health = self.health - damage;
1002         WaypointSprite_UpdateHealth(self.sprite, self.health);
1003         // choose an animation frame based on health
1004         self.frame = 10 * bound(0, (1 - self.health / self.max_health), 1);
1005         // see if the generator is still functional, or dying
1006         if (self.health > 0)
1007         {
1008                 self.lasthealth = self.health;
1009         }
1010         else
1011         {
1012                 if (attacker == self)
1013                         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(self.team, INFO_ONSLAUGHT_GENDESTROYED_OVERTIME_));
1014                 else
1015                 {
1016                         Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(self.team, INFO_ONSLAUGHT_GENDESTROYED_));
1017                         PlayerScore_Add(attacker, SP_SCORE, 100);
1018                 }
1019                 self.iscaptured = false;
1020                 self.islinked = false;
1021                 self.isshielded = false;
1022                 self.takedamage = DAMAGE_NO; // can't be hurt anymore
1023                 self.event_damage = func_null; // won't do anything if hurt
1024                 self.count = 0; // reset counter
1025                 self.think = func_null;
1026                 self.nextthink = 0;
1027                 //self.think(); // do the first explosion now
1028
1029                 WaypointSprite_UpdateMaxHealth(self.sprite, 0);
1030                 WaypointSprite_Ping(self.sprite);
1031                 //WaypointSprite_Kill(self.sprite); // can't do this yet, code too poor
1032
1033                 onslaught_updatelinks();
1034         }
1035
1036         // Throw some flaming gibs on damage, more damage = more chance for gib
1037         if(random() < damage/220)
1038         {
1039                 sound(self, CH_TRIGGER, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM);
1040         }
1041         else
1042         {
1043                 // particles on every hit
1044                 Send_Effect(EFFECT_SPARKS, hitloc, force * -1, 1);
1045
1046                 //sound on every hit
1047                 if (random() < 0.5)
1048                         sound(self, CH_TRIGGER, SND_ONS_HIT1, VOL_BASE, ATTEN_NORM);
1049                 else
1050                         sound(self, CH_TRIGGER, SND_ONS_HIT2, VOL_BASE, ATTEN_NORM);
1051         }
1052
1053         self.SendFlags |= GSF_STATUS;
1054 }
1055
1056 void ons_GeneratorThink()
1057 {SELFPARAM();
1058         entity e;
1059         self.nextthink = time + GEN_THINKRATE;
1060         if (!gameover)
1061         {
1062         if(!self.isshielded && self.wait < time)
1063         {
1064             self.wait = time + 5;
1065             FOR_EACH_REALPLAYER(e)
1066             {
1067                                 if(SAME_TEAM(e, self))
1068                                 {
1069                                         Send_Notification(NOTIF_ONE, e, MSG_CENTER, CENTER_ONS_NOTSHIELDED_TEAM);
1070                     soundto(MSG_ONE, e, CHAN_AUTO, SND(KH_ALARM), VOL_BASE, ATTEN_NONE);    // FIXME: unique sound?
1071                 }
1072                                 else
1073                                         Send_Notification(NOTIF_ONE, e, MSG_CENTER, APP_TEAM_NUM_4(self.team, CENTER_ONS_NOTSHIELDED_));
1074             }
1075         }
1076         }
1077 }
1078
1079 void ons_GeneratorReset(entity this)
1080 {
1081         this.team = this.team_saved;
1082         this.lasthealth = this.max_health = this.health = autocvar_g_onslaught_gen_health;
1083         this.takedamage = DAMAGE_AIM;
1084         this.bot_attack = true;
1085         this.iscaptured = true;
1086         this.islinked = true;
1087         this.isshielded = true;
1088         this.event_damage = ons_GeneratorDamage;
1089         this.think = ons_GeneratorThink;
1090         this.nextthink = time + GEN_THINKRATE;
1091
1092         Net_LinkEntity(this, false, 0, generator_send);
1093
1094         this.SendFlags = GSF_SETUP; // just incase
1095         this.SendFlags |= GSF_STATUS;
1096
1097         WaypointSprite_UpdateMaxHealth(this.sprite, this.max_health);
1098         WaypointSprite_UpdateHealth(this.sprite, this.health);
1099         WaypointSprite_UpdateRule(this.sprite,this.team,SPRITERULE_TEAMPLAY);
1100
1101         onslaught_updatelinks();
1102 }
1103
1104 void ons_DelayedGeneratorSetup()
1105 {SELFPARAM();
1106         // bot waypoints
1107         waypoint_spawnforitem_force(self, self.origin);
1108         self.nearestwaypointtimeout = 0; // activate waypointing again
1109         self.bot_basewaypoint = self.nearestwaypoint;
1110
1111         // captureshield setup
1112         ons_CaptureShield_Spawn(self, true);
1113
1114         onslaught_updatelinks();
1115
1116         Net_LinkEntity(self, false, 0, generator_send);
1117 }
1118
1119
1120 void onslaught_generator_touch()
1121 {SELFPARAM();
1122         if ( IS_PLAYER(other) )
1123         if ( SAME_TEAM(self,other) )
1124         if ( self.iscaptured )
1125         {
1126                 Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_ONS_TELEPORT);
1127         }
1128 }
1129
1130 void ons_GeneratorSetup(entity gen) // called when spawning a generator entity on the map as a spawnfunc
1131 {SELFPARAM();
1132         // declarations
1133         int teamnumber = gen.team;
1134         setself(gen); // for later usage with droptofloor()
1135
1136         // main setup
1137         gen.ons_worldgeneratornext = ons_worldgeneratorlist; // link generator into ons_worldgeneratorlist
1138         ons_worldgeneratorlist = gen;
1139
1140         gen.netname = sprintf("%s generator", Team_ColoredFullName(teamnumber));
1141         gen.classname = "onslaught_generator";
1142         gen.solid = SOLID_BBOX;
1143         gen.team_saved = teamnumber;
1144         gen.movetype = MOVETYPE_NONE;
1145         gen.lasthealth = gen.max_health = gen.health = autocvar_g_onslaught_gen_health;
1146         gen.takedamage = DAMAGE_AIM;
1147         gen.bot_attack = true;
1148         gen.event_damage = ons_GeneratorDamage;
1149         gen.reset = ons_GeneratorReset;
1150         gen.think = ons_GeneratorThink;
1151         gen.nextthink = time + GEN_THINKRATE;
1152         gen.iscaptured = true;
1153         gen.islinked = true;
1154         gen.isshielded = true;
1155         gen.touch = onslaught_generator_touch;
1156
1157         // appearence
1158         // model handled by CSQC
1159         setsize(gen, GENERATOR_MIN, GENERATOR_MAX);
1160         setorigin(gen, (gen.origin + CPGEN_SPAWN_OFFSET));
1161         gen.colormap = 1024 + (teamnumber - 1) * 17;
1162
1163         // generator placement
1164         setself(gen);
1165         droptofloor();
1166
1167         // waypointsprites
1168         WaypointSprite_SpawnFixed(WP_Null, self.origin + CPGEN_WAYPOINT_OFFSET, self, sprite, RADARICON_NONE);
1169         WaypointSprite_UpdateRule(self.sprite, self.team, SPRITERULE_TEAMPLAY);
1170         WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
1171         WaypointSprite_UpdateHealth(self.sprite, self.health);
1172
1173         InitializeEntity(gen, ons_DelayedGeneratorSetup, INITPRIO_SETLOCATION);
1174 }
1175
1176
1177 // ===============
1178 //  Round Handler
1179 // ===============
1180
1181 int total_generators;
1182 void Onslaught_count_generators()
1183 {
1184         entity e;
1185         total_generators = redowned = blueowned = yellowowned = pinkowned = 0;
1186         for(e = ons_worldgeneratorlist; e; e = e.ons_worldgeneratornext)
1187         {
1188                 ++total_generators;
1189                 redowned += (e.team == NUM_TEAM_1 && e.health > 0);
1190                 blueowned += (e.team == NUM_TEAM_2 && e.health > 0);
1191                 yellowowned += (e.team == NUM_TEAM_3 && e.health > 0);
1192                 pinkowned += (e.team == NUM_TEAM_4 && e.health > 0);
1193         }
1194 }
1195
1196 int Onslaught_GetWinnerTeam()
1197 {
1198         int winner_team = 0;
1199         if(redowned > 0)
1200                 winner_team = NUM_TEAM_1;
1201         if(blueowned > 0)
1202         {
1203                 if(winner_team) return 0;
1204                 winner_team = NUM_TEAM_2;
1205         }
1206         if(yellowowned > 0)
1207         {
1208                 if(winner_team) return 0;
1209                 winner_team = NUM_TEAM_3;
1210         }
1211         if(pinkowned > 0)
1212         {
1213                 if(winner_team) return 0;
1214                 winner_team = NUM_TEAM_4;
1215         }
1216         if(winner_team)
1217                 return winner_team;
1218         return -1; // no generators left?
1219 }
1220
1221 void nades_Clear(entity e);
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