]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/mutators/mutator/gamemode_onslaught.qc
Merge branch 'master' into TimePath/debug_draw
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / mutators / mutator / gamemode_onslaught.qc
1 #ifndef GAMEMODE_ONSLAUGHT_H
2 #define GAMEMODE_ONSLAUGHT_H
3
4 float autocvar_g_onslaught_point_limit;
5 void ons_Initialize();
6
7 REGISTER_MUTATOR(ons, false)
8 {
9         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 "../../controlpoint.qh"
132 #include "../../generator.qh"
133
134 bool g_onslaught;
135
136 float autocvar_g_onslaught_debug;
137 float autocvar_g_onslaught_teleport_wait;
138 bool autocvar_g_onslaught_spawn_at_controlpoints;
139 bool autocvar_g_onslaught_spawn_at_generator;
140 float autocvar_g_onslaught_cp_proxydecap;
141 float autocvar_g_onslaught_cp_proxydecap_distance = 512;
142 float autocvar_g_onslaught_cp_proxydecap_dps = 100;
143 float autocvar_g_onslaught_spawn_at_controlpoints_chance = 0.5;
144 float autocvar_g_onslaught_spawn_at_controlpoints_random;
145 float autocvar_g_onslaught_spawn_at_generator_chance;
146 float autocvar_g_onslaught_spawn_at_generator_random;
147 float autocvar_g_onslaught_cp_buildhealth;
148 float autocvar_g_onslaught_cp_buildtime;
149 float autocvar_g_onslaught_cp_health;
150 float autocvar_g_onslaught_cp_regen;
151 float autocvar_g_onslaught_gen_health;
152 float autocvar_g_onslaught_shield_force = 100;
153 float autocvar_g_onslaught_allow_vehicle_touch;
154 float autocvar_g_onslaught_round_timelimit;
155 float autocvar_g_onslaught_warmup;
156 float autocvar_g_onslaught_teleport_radius;
157 float autocvar_g_onslaught_spawn_choose;
158 float autocvar_g_onslaught_click_radius;
159
160 void FixSize(entity e);
161
162 // =======================
163 // CaptureShield Functions
164 // =======================
165
166 bool ons_CaptureShield_Customize()
167 {SELFPARAM();
168         entity e = WaypointSprite_getviewentity(other);
169
170         if(!self.enemy.isshielded && (ons_ControlPoint_Attackable(self.enemy, e.team) > 0 || self.enemy.classname != "onslaught_controlpoint")) { return false; }
171         if(SAME_TEAM(self, e)) { return false; }
172
173         return true;
174 }
175
176 void ons_CaptureShield_Touch()
177 {SELFPARAM();
178         if(!self.enemy.isshielded && (ons_ControlPoint_Attackable(self.enemy, other.team) > 0 || self.enemy.classname != "onslaught_controlpoint")) { return; }
179         if(!IS_PLAYER(other)) { return; }
180         if(SAME_TEAM(other, self)) { return; }
181
182         vector mymid = (self.absmin + self.absmax) * 0.5;
183         vector othermid = (other.absmin + other.absmax) * 0.5;
184
185         Damage(other, self, self, 0, DEATH_HURTTRIGGER.m_id, mymid, normalize(othermid - mymid) * ons_captureshield_force);
186
187         if(IS_REAL_CLIENT(other))
188         {
189                 play2(other, SND(ONS_DAMAGEBLOCKEDBYSHIELD));
190
191                 if(self.enemy.classname == "onslaught_generator")
192                         Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_ONS_GENERATOR_SHIELDED);
193                 else
194                         Send_Notification(NOTIF_ONE, other, MSG_CENTER, CENTER_ONS_CONTROLPOINT_SHIELDED);
195         }
196 }
197
198 void ons_CaptureShield_Reset()
199 {SELFPARAM();
200         self.colormap = self.enemy.colormap;
201         self.team = self.enemy.team;
202 }
203
204 void ons_CaptureShield_Spawn(entity generator, bool is_generator)
205 {
206         entity shield = 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         WriteByte(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()
849 {SELFPARAM();
850         if(self.goalentity)
851                 remove(self.goalentity);
852
853         self.goalentity = world;
854         self.team = 0;
855         self.colormap = 1024;
856         self.iscaptured = false;
857         self.islinked = false;
858         self.isshielded = true;
859         self.think = ons_ControlPoint_Think;
860         self.ons_toucher = world;
861         self.nextthink = time + ONS_CP_THINKRATE;
862         setmodel_fixsize(self, MDL_ONS_CP_PAD1);
863
864         WaypointSprite_UpdateMaxHealth(self.sprite, 0);
865         WaypointSprite_UpdateRule(self.sprite,self.team,SPRITERULE_TEAMPLAY);
866
867         onslaught_updatelinks();
868
869         activator = self;
870         SUB_UseTargets(); // to reset the structures, playerspawns etc.
871
872         CSQCMODEL_AUTOUPDATE(self);
873 }
874
875 void ons_DelayedControlPoint_Setup(void)
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()
1080 {SELFPARAM();
1081         self.team = self.team_saved;
1082         self.lasthealth = self.max_health = self.health = autocvar_g_onslaught_gen_health;
1083         self.takedamage = DAMAGE_AIM;
1084         self.bot_attack = true;
1085         self.iscaptured = true;
1086         self.islinked = true;
1087         self.isshielded = true;
1088         self.event_damage = ons_GeneratorDamage;
1089         self.think = ons_GeneratorThink;
1090         self.nextthink = time + GEN_THINKRATE;
1091
1092         Net_LinkEntity(self, false, 0, generator_send);
1093
1094         self.SendFlags = GSF_SETUP; // just incase
1095         self.SendFlags |= GSF_STATUS;
1096
1097         WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
1098         WaypointSprite_UpdateHealth(self.sprite, self.health);
1099         WaypointSprite_UpdateRule(self.sprite,self.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 #define ONS_OWNED_GENERATORS() ((redowned > 0) + (blueowned > 0) + (yellowowned > 0) + (pinkowned > 0))
1222 #define ONS_OWNED_GENERATORS_OK() (ONS_OWNED_GENERATORS() > 1)
1223 bool Onslaught_CheckWinner()
1224 {
1225         entity e;
1226
1227         if ((autocvar_timelimit && time > game_starttime + autocvar_timelimit * 60) || (round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0))
1228         {
1229                 ons_stalemate = true;
1230
1231                 if (!wpforenemy_announced)
1232                 {
1233                         Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT);
1234                         sound(world, CH_INFO, SND_ONS_GENERATOR_DECAY, VOL_BASE, ATTEN_NONE);
1235
1236                         wpforenemy_announced = true;
1237                 }
1238
1239                 entity tmp_entity; // temporary entity
1240                 float d;
1241                 for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext) if(time >= tmp_entity.ons_overtime_damagedelay)
1242                 {
1243                         // tmp_entity.max_health / 300 gives 5 minutes of overtime.
1244                         // control points reduce the overtime duration.
1245                         d = 1;
1246                         for(e = ons_worldcplist; e; e = e.ons_worldcpnext)
1247                         {
1248                                 if(DIFF_TEAM(e, tmp_entity))
1249                                 if(e.islinked)
1250                                         d = d + 1;
1251                         }
1252
1253                         if(autocvar_g_campaign && autocvar__campaign_testrun)
1254                                 d = d * tmp_entity.max_health;
1255                         else
1256                                 d = d * tmp_entity.max_health / max(30, 60 * autocvar_timelimit_suddendeath);
1257
1258                         Damage(tmp_entity, tmp_entity, tmp_entity, d, DEATH_HURTTRIGGER.m_id, tmp_entity.origin, '0 0 0');
1259
1260                         tmp_entity.sprite.SendFlags |= 16;
1261
1262                         tmp_entity.ons_overtime_damagedelay = time + 1;
1263                 }
1264         }
1265         else { wpforenemy_announced = false; ons_stalemate = false; }
1266
1267         Onslaught_count_generators();
1268
1269         if(ONS_OWNED_GENERATORS_OK())
1270                 return 0;
1271
1272         int winner_team = Onslaught_GetWinnerTeam();
1273
1274         if(winner_team > 0)
1275         {
1276                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, APP_TEAM_NUM_4(winner_team, CENTER_ROUND_TEAM_WIN_));
1277                 Send_Notification(NOTIF_ALL, world, MSG_INFO, APP_TEAM_NUM_4(winner_team, INFO_ROUND_TEAM_WIN_));
1278                 TeamScore_AddToTeam(winner_team, ST_ONS_CAPS, +1);
1279         }
1280         else if(winner_team == -1)
1281         {
1282                 Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_TIED);
1283                 Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_TIED);
1284         }
1285
1286         ons_stalemate = false;
1287
1288         play2all(SND(CTF_CAPTURE(winner_team)));
1289
1290         round_handler_Init(7, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit);
1291
1292         FOR_EACH_PLAYER(e)
1293         {
1294                 e.ons_roundlost = true;
1295                 e.player_blocked = true;
1296
1297                 nades_Clear(e);
1298         }
1299
1300         return 1;
1301 }
1302
1303 bool Onslaught_CheckPlayers()
1304 {
1305         return 1;
1306 }
1307
1308 void Onslaught_RoundStart()
1309 {
1310         entity tmp_entity;
1311         FOR_EACH_PLAYER(tmp_entity) { tmp_entity.player_blocked = false; }
1312
1313         for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext)
1314                 tmp_entity.sprite.SendFlags |= 16;
1315
1316         for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
1317                 tmp_entity.sprite.SendFlags |= 16;
1318 }
1319
1320
1321 // ================
1322 // Bot player logic
1323 // ================
1324
1325 // NOTE: LEGACY CODE, needs to be re-written!
1326
1327 void havocbot_goalrating_ons_offenseitems(float ratingscale, vector org, float sradius)
1328 {SELFPARAM();
1329         entity head;
1330         float t, c;
1331         int i;
1332         bool needarmor = false, needweapons = false;
1333
1334         // Needs armor/health?
1335         if(self.health<100)
1336                 needarmor = true;
1337
1338         // Needs weapons?
1339         c = 0;
1340         for(i = WEP_FIRST; i <= WEP_LAST ; ++i)
1341         {
1342                 // Find weapon
1343                 if(self.weapons & WepSet_FromWeapon(i))
1344                 if(++c>=4)
1345                         break;
1346         }
1347
1348         if(c<4)
1349                 needweapons = true;
1350
1351         if(!needweapons && !needarmor)
1352                 return;
1353
1354         ons_debug(strcat(self.netname, " needs weapons ", ftos(needweapons) , "\n"));
1355         ons_debug(strcat(self.netname, " needs armor ", ftos(needarmor) , "\n"));
1356
1357         // See what is around
1358         head = findchainfloat(bot_pickup, true);
1359         while (head)
1360         {
1361                 // gather health and armor only
1362                 if (head.solid)
1363                 if ( ((head.health || head.armorvalue) && needarmor) || (head.weapons && needweapons ) )
1364                 if (vlen(head.origin - org) < sradius)
1365                 {
1366                         t = head.bot_pickupevalfunc(self, head);
1367                         if (t > 0)
1368                                 navigation_routerating(head, t * ratingscale, 500);
1369                 }
1370                 head = head.chain;
1371         }
1372 }
1373
1374 void havocbot_role_ons_setrole(entity bot, int role)
1375 {
1376         ons_debug(strcat(bot.netname," switched to "));
1377         switch(role)
1378         {
1379                 case HAVOCBOT_ONS_ROLE_DEFENSE:
1380                         ons_debug("defense");
1381                         bot.havocbot_role = havocbot_role_ons_defense;
1382                         bot.havocbot_role_flags = HAVOCBOT_ONS_ROLE_DEFENSE;
1383                         bot.havocbot_role_timeout = 0;
1384                         break;
1385                 case HAVOCBOT_ONS_ROLE_ASSISTANT:
1386                         ons_debug("assistant");
1387                         bot.havocbot_role = havocbot_role_ons_assistant;
1388                         bot.havocbot_role_flags = HAVOCBOT_ONS_ROLE_ASSISTANT;
1389                         bot.havocbot_role_timeout = 0;
1390                         break;
1391                 case HAVOCBOT_ONS_ROLE_OFFENSE:
1392                         ons_debug("offense");
1393                         bot.havocbot_role = havocbot_role_ons_offense;
1394                         bot.havocbot_role_flags = HAVOCBOT_ONS_ROLE_OFFENSE;
1395                         bot.havocbot_role_timeout = 0;
1396                         break;
1397         }
1398         ons_debug("\n");
1399 }
1400
1401 int havocbot_ons_teamcount(entity bot, int role)
1402 {SELFPARAM();
1403         int c = 0;
1404         entity head;
1405
1406         FOR_EACH_PLAYER(head)
1407         if(SAME_TEAM(head, self))
1408         if(head.havocbot_role_flags & role)
1409                 ++c;
1410
1411         return c;
1412 }
1413
1414 void havocbot_goalrating_ons_controlpoints_attack(float ratingscale)
1415 {SELFPARAM();
1416         entity cp, cp1, cp2, best, pl, wp;
1417         float radius, bestvalue;
1418         int c;
1419         bool found;
1420
1421         // Filter control points
1422         for(cp2 = ons_worldcplist; cp2; cp2 = cp2.ons_worldcpnext)
1423         {
1424                 cp2.wpcost = c = 0;
1425                 cp2.wpconsidered = false;
1426
1427                 if(cp2.isshielded)
1428                         continue;
1429
1430                 // Ignore owned controlpoints
1431                 if(!(cp2.isgenneighbor[self.team] || cp2.iscpneighbor[self.team]))
1432                         continue;
1433
1434                 // Count team mates interested in this control point
1435                 // (easier and cleaner than keeping counters per cp and teams)
1436                 FOR_EACH_PLAYER(pl)
1437                 if(SAME_TEAM(pl, self))
1438                 if(pl.havocbot_role_flags & HAVOCBOT_ONS_ROLE_OFFENSE)
1439                 if(pl.havocbot_ons_target==cp2)
1440                         ++c;
1441
1442                 // NOTE: probably decrease the cost of attackable control points
1443                 cp2.wpcost = c;
1444                 cp2.wpconsidered = true;
1445         }
1446
1447         // We'll consider only the best case
1448         bestvalue = 99999999999;
1449         cp = world;
1450         for(cp1 = ons_worldcplist; cp1; cp1 = cp1.ons_worldcpnext)
1451         {
1452                 if (!cp1.wpconsidered)
1453                         continue;
1454
1455                 if(cp1.wpcost<bestvalue)
1456                 {
1457                         bestvalue = cp1.wpcost;
1458                         cp = cp1;
1459                         self.havocbot_ons_target = cp1;
1460                 }
1461         }
1462
1463         if (!cp)
1464                 return;
1465
1466         ons_debug(strcat(self.netname, " chose cp ranked ", ftos(bestvalue), "\n"));
1467
1468         if(cp.goalentity)
1469         {
1470                 // Should be attacked
1471                 // Rate waypoints near it
1472                 found = false;
1473                 best = world;
1474                 bestvalue = 99999999999;
1475                 for(radius=0; radius<1000 && !found; radius+=500)
1476                 {
1477                         for(wp=findradius(cp.origin,radius); wp; wp=wp.chain)
1478                         {
1479                                 if(!(wp.wpflags & WAYPOINTFLAG_GENERATED))
1480                                 if(wp.classname=="waypoint")
1481                                 if(checkpvs(wp.origin,cp))
1482                                 {
1483                                         found = true;
1484                                         if(wp.cnt<bestvalue)
1485                                         {
1486                                                 best = wp;
1487                                                 bestvalue = wp.cnt;
1488                                         }
1489                                 }
1490                         }
1491                 }
1492
1493                 if(best)
1494                 {
1495                         navigation_routerating(best, ratingscale, 10000);
1496                         best.cnt += 1;
1497
1498                         self.havocbot_attack_time = 0;
1499                         if(checkpvs(self.view_ofs,cp))
1500                         if(checkpvs(self.view_ofs,best))
1501                                 self.havocbot_attack_time = time + 2;
1502                 }
1503                 else
1504                 {
1505                         navigation_routerating(cp, ratingscale, 10000);
1506                 }
1507                 ons_debug(strcat(self.netname, " found an attackable controlpoint at ", vtos(cp.origin) ,"\n"));
1508         }
1509         else
1510         {
1511                 // Should be touched
1512                 ons_debug(strcat(self.netname, " found a touchable controlpoint at ", vtos(cp.origin) ,"\n"));
1513                 found = false;
1514
1515                 // Look for auto generated waypoint
1516                 if (!bot_waypoints_for_items)
1517                 for (wp = findradius(cp.origin,100); wp; wp = wp.chain)
1518                 {
1519                         if(wp.classname=="waypoint")
1520                         {
1521                                 navigation_routerating(wp, ratingscale, 10000);
1522                                 found = true;
1523                         }
1524                 }
1525
1526                 // Nothing found, rate the controlpoint itself
1527                 if (!found)
1528                         navigation_routerating(cp, ratingscale, 10000);
1529         }
1530 }
1531
1532 bool havocbot_goalrating_ons_generator_attack(float ratingscale)
1533 {SELFPARAM();
1534         entity g, wp, bestwp;
1535         bool found;
1536         int best;
1537
1538         for(g = ons_worldgeneratorlist; g; g = g.ons_worldgeneratornext)
1539         {
1540                 if(SAME_TEAM(g, self) || g.isshielded)
1541                         continue;
1542
1543                 // Should be attacked
1544                 // Rate waypoints near it
1545                 found = false;
1546                 bestwp = world;
1547                 best = 99999999999;
1548
1549                 for(wp=findradius(g.origin,400); wp; wp=wp.chain)
1550                 {
1551                         if(wp.classname=="waypoint")
1552                         if(checkpvs(wp.origin,g))
1553                         {
1554                                 found = true;
1555                                 if(wp.cnt<best)
1556                                 {
1557                                         bestwp = wp;
1558                                         best = wp.cnt;
1559                                 }
1560                         }
1561                 }
1562
1563                 if(bestwp)
1564                 {
1565                         ons_debug("waypoints found around generator\n");
1566                         navigation_routerating(bestwp, ratingscale, 10000);
1567                         bestwp.cnt += 1;
1568
1569                         self.havocbot_attack_time = 0;
1570                         if(checkpvs(self.view_ofs,g))
1571                         if(checkpvs(self.view_ofs,bestwp))
1572                                 self.havocbot_attack_time = time + 5;
1573
1574                         return true;
1575                 }
1576                 else
1577                 {
1578                         ons_debug("generator found without waypoints around\n");
1579                         // if there aren't waypoints near the generator go straight to it
1580                         navigation_routerating(g, ratingscale, 10000);
1581                         self.havocbot_attack_time = 0;
1582                         return true;
1583                 }
1584         }
1585         return false;
1586 }
1587
1588 void havocbot_role_ons_offense()
1589 {SELFPARAM();
1590         if(self.deadflag != DEAD_NO)
1591         {
1592                 self.havocbot_attack_time = 0;
1593                 havocbot_ons_reset_role(self);
1594                 return;
1595         }
1596
1597         // Set the role timeout if necessary
1598         if (!self.havocbot_role_timeout)
1599                 self.havocbot_role_timeout = time + 120;
1600
1601         if (time > self.havocbot_role_timeout)
1602         {
1603                 havocbot_ons_reset_role(self);
1604                 return;
1605         }
1606
1607         if(self.havocbot_attack_time>time)
1608                 return;
1609
1610         if (self.bot_strategytime < time)
1611         {
1612                 navigation_goalrating_start();
1613                 havocbot_goalrating_enemyplayers(20000, self.origin, 650);
1614                 if(!havocbot_goalrating_ons_generator_attack(20000))
1615                         havocbot_goalrating_ons_controlpoints_attack(20000);
1616                 havocbot_goalrating_ons_offenseitems(10000, self.origin, 10000);
1617                 navigation_goalrating_end();
1618
1619                 self.bot_strategytime = time + autocvar_bot_ai_strategyinterval;
1620         }
1621 }
1622
1623 void havocbot_role_ons_assistant()
1624 {SELFPARAM();
1625         havocbot_ons_reset_role(self);
1626 }
1627
1628 void havocbot_role_ons_defense()
1629 {SELFPARAM();
1630         havocbot_ons_reset_role(self);
1631 }
1632
1633 void havocbot_ons_reset_role(entity bot)
1634 {SELFPARAM();
1635         entity head;
1636         int c = 0;
1637
1638         if(self.deadflag != DEAD_NO)
1639                 return;
1640
1641         bot.havocbot_ons_target = world;
1642
1643         // TODO: Defend control points or generator if necessary
1644
1645         // if there is only me on the team switch to offense
1646         c = 0;
1647         FOR_EACH_PLAYER(head)
1648         if(SAME_TEAM(head, self))
1649                 ++c;
1650
1651         if(c==1)
1652         {
1653                 havocbot_role_ons_setrole(bot, HAVOCBOT_ONS_ROLE_OFFENSE);
1654                 return;
1655         }
1656
1657         havocbot_role_ons_setrole(bot, HAVOCBOT_ONS_ROLE_OFFENSE);
1658 }
1659
1660
1661 /*
1662  * Find control point or generator owned by the same team self which is nearest to pos
1663  * if max_dist is positive, only control points within this range will be considered
1664  */
1665 entity ons_Nearest_ControlPoint(vector pos, float max_dist)
1666 {SELFPARAM();
1667         entity tmp_entity, closest_target = world;
1668         tmp_entity = findchain(classname, "onslaught_controlpoint");
1669         while(tmp_entity)
1670         {
1671                 if(SAME_TEAM(tmp_entity, self))
1672                 if(tmp_entity.iscaptured)
1673                 if(max_dist <= 0 || vlen(tmp_entity.origin - pos) <= max_dist)
1674                 if(vlen(tmp_entity.origin - pos) <= vlen(closest_target.origin - pos) || closest_target == world)
1675                         closest_target = tmp_entity;
1676                 tmp_entity = tmp_entity.chain;
1677         }
1678         tmp_entity = findchain(classname, "onslaught_generator");
1679         while(tmp_entity)
1680         {
1681                 if(SAME_TEAM(tmp_entity, self))
1682                 if(max_dist <= 0 || vlen(tmp_entity.origin - pos) < max_dist)
1683                 if(vlen(tmp_entity.origin - pos) <= vlen(closest_target.origin - pos) || closest_target == world)
1684                         closest_target = tmp_entity;
1685                 tmp_entity = tmp_entity.chain;
1686         }
1687
1688         return closest_target;
1689 }
1690
1691 /*
1692  * Find control point or generator owned by the same team self which is nearest to pos
1693  * if max_dist is positive, only control points within this range will be considered
1694  * This function only check distances on the XY plane, disregarding Z
1695  */
1696 entity ons_Nearest_ControlPoint_2D(vector pos, float max_dist)
1697 {SELFPARAM();
1698         entity tmp_entity, closest_target = world;
1699         vector delta;
1700         float smallest_distance = 0, distance;
1701
1702         tmp_entity = findchain(classname, "onslaught_controlpoint");
1703         while(tmp_entity)
1704         {
1705                 delta = tmp_entity.origin - pos;
1706                 delta_z = 0;
1707                 distance = vlen(delta);
1708
1709                 if(SAME_TEAM(tmp_entity, self))
1710                 if(tmp_entity.iscaptured)
1711                 if(max_dist <= 0 || distance <= max_dist)
1712                 if(closest_target == world || distance <= smallest_distance )
1713                 {
1714                         closest_target = tmp_entity;
1715                         smallest_distance = distance;
1716                 }
1717
1718                 tmp_entity = tmp_entity.chain;
1719         }
1720         tmp_entity = findchain(classname, "onslaught_generator");
1721         while(tmp_entity)
1722         {
1723                 delta = tmp_entity.origin - pos;
1724                 delta_z = 0;
1725                 distance = vlen(delta);
1726
1727                 if(SAME_TEAM(tmp_entity, self))
1728                 if(max_dist <= 0 || distance <= max_dist)
1729                 if(closest_target == world || distance <= smallest_distance )
1730                 {
1731                         closest_target = tmp_entity;
1732                         smallest_distance = distance;
1733                 }
1734
1735                 tmp_entity = tmp_entity.chain;
1736         }
1737
1738         return closest_target;
1739 }
1740 /**
1741  * find the number of control points and generators in the same team as self
1742  */
1743 int ons_Count_SelfControlPoints()
1744 {SELFPARAM();
1745         entity tmp_entity;
1746         tmp_entity = findchain(classname, "onslaught_controlpoint");
1747         int n = 0;
1748         while(tmp_entity)
1749         {
1750                 if(SAME_TEAM(tmp_entity, self))
1751                 if(tmp_entity.iscaptured)
1752                         n++;
1753                 tmp_entity = tmp_entity.chain;
1754         }
1755         tmp_entity = findchain(classname, "onslaught_generator");
1756         while(tmp_entity)
1757         {
1758                 if(SAME_TEAM(tmp_entity, self))
1759                         n++;
1760                 tmp_entity = tmp_entity.chain;
1761         }
1762         return n;
1763 }
1764
1765 /**
1766  * Teleport player to a random position near tele_target
1767  * if tele_effects is true, teleport sound+particles are created
1768  * return false on failure
1769  */
1770 bool ons_Teleport(entity player, entity tele_target, float range, bool tele_effects)
1771 {
1772         if ( !tele_target )
1773                 return false;
1774
1775         int i;
1776         vector loc;
1777         float theta;
1778         // narrow the range for each iteration to increase chances that a spawnpoint
1779         // can be found even if there's little room around the control point
1780         float iteration_scale = 1;
1781         for(i = 0; i < 16; ++i)
1782         {
1783                 iteration_scale -= i / 16;
1784                 theta = random() * 2 * M_PI;
1785                 loc_y = sin(theta);
1786                 loc_x = cos(theta);
1787                 loc_z = 0;
1788                 loc *= random() * range * iteration_scale;
1789
1790                 loc += tele_target.origin + '0 0 128' * iteration_scale;
1791
1792                 tracebox(loc, PL_MIN, PL_MAX, loc, MOVE_NORMAL, player);
1793                 if(trace_fraction == 1.0 && !trace_startsolid)
1794                 {
1795                         traceline(tele_target.origin, loc, MOVE_NOMONSTERS, tele_target); // double check to make sure we're not spawning outside the world
1796                         if(trace_fraction == 1.0 && !trace_startsolid)
1797                         {
1798                                 if ( tele_effects )
1799                                 {
1800                                         Send_Effect(EFFECT_TELEPORT, player.origin, '0 0 0', 1);
1801                                         sound (player, CH_TRIGGER, SND_TELEPORT, VOL_BASE, ATTEN_NORM);
1802                                 }
1803                                 setorigin(player, loc);
1804                                 player.angles = '0 1 0' * ( theta * RAD2DEG + 180 );
1805                                 makevectors(player.angles);
1806                                 player.fixangle = true;
1807                                 player.teleport_antispam = time + autocvar_g_onslaught_teleport_wait;
1808
1809                                 if ( tele_effects )
1810                                         Send_Effect(EFFECT_TELEPORT, player.origin + v_forward * 32, '0 0 0', 1);
1811                                 return true;
1812                         }
1813                 }
1814         }
1815
1816         return false;
1817 }
1818
1819 // ==============
1820 // Hook Functions
1821 // ==============
1822
1823 MUTATOR_HOOKFUNCTION(ons, reset_map_global)
1824 {SELFPARAM();
1825         entity e;
1826         FOR_EACH_PLAYER(e)
1827         {
1828                 e.ons_roundlost = false;
1829                 e.ons_deathloc = '0 0 0';
1830                 WITH(entity, self, e, PutClientInServer());
1831         }
1832         return false;
1833 }
1834
1835 MUTATOR_HOOKFUNCTION(ons, ClientDisconnect)
1836 {SELFPARAM();
1837         self.ons_deathloc = '0 0 0';
1838         return false;
1839 }
1840
1841 MUTATOR_HOOKFUNCTION(ons, MakePlayerObserver)
1842 {SELFPARAM();
1843         self.ons_deathloc = '0 0 0';
1844         return false;
1845 }
1846
1847 MUTATOR_HOOKFUNCTION(ons, PlayerSpawn)
1848 {SELFPARAM();
1849         if(!round_handler_IsRoundStarted())
1850         {
1851                 self.player_blocked = true;
1852                 return false;
1853         }
1854
1855         entity l;
1856         for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
1857         {
1858                 l.sprite.SendFlags |= 16;
1859         }
1860         for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
1861         {
1862                 l.sprite.SendFlags |= 16;
1863         }
1864
1865         if(ons_stalemate) { Send_Notification(NOTIF_ONE, self, MSG_CENTER, CENTER_OVERTIME_CONTROLPOINT); }
1866
1867         if ( autocvar_g_onslaught_spawn_choose )
1868         if ( self.ons_spawn_by )
1869         if ( ons_Teleport(self,self.ons_spawn_by,autocvar_g_onslaught_teleport_radius,false) )
1870         {
1871                 self.ons_spawn_by = world;
1872                 return false;
1873         }
1874
1875         if(autocvar_g_onslaught_spawn_at_controlpoints)
1876         if(random() <= autocvar_g_onslaught_spawn_at_controlpoints_chance)
1877         {
1878                 float random_target = autocvar_g_onslaught_spawn_at_controlpoints_random;
1879                 entity tmp_entity, closest_target = world;
1880                 vector spawn_loc = self.ons_deathloc;
1881
1882                 // new joining player or round reset, don't bother checking
1883                 if(spawn_loc == '0 0 0') { return false; }
1884
1885                 if(random_target) { RandomSelection_Init(); }
1886
1887                 for(tmp_entity = ons_worldcplist; tmp_entity; tmp_entity = tmp_entity.ons_worldcpnext)
1888                 {
1889                         if(SAME_TEAM(tmp_entity, self))
1890                         if(random_target)
1891                                 RandomSelection_Add(tmp_entity, 0, string_null, 1, 1);
1892                         else if(vlen(tmp_entity.origin - spawn_loc) <= vlen(closest_target.origin - spawn_loc) || closest_target == world)
1893                                 closest_target = tmp_entity;
1894                 }
1895
1896                 if(random_target) { closest_target = RandomSelection_chosen_ent; }
1897
1898                 if(closest_target)
1899                 {
1900                         float i;
1901                         vector loc;
1902                         float iteration_scale = 1;
1903                         for(i = 0; i < 10; ++i)
1904                         {
1905                                 iteration_scale -= i / 10;
1906                                 loc = closest_target.origin + '0 0 96' * iteration_scale;
1907                                 loc += ('0 1 0' * random()) * 128 * iteration_scale;
1908                                 tracebox(loc, PL_MIN, PL_MAX, loc, MOVE_NORMAL, self);
1909                                 if(trace_fraction == 1.0 && !trace_startsolid)
1910                                 {
1911                                         traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the world
1912                                         if(trace_fraction == 1.0 && !trace_startsolid)
1913                                         {
1914                                                 setorigin(self, loc);
1915                                                 self.angles = normalize(loc - closest_target.origin) * RAD2DEG;
1916                                                 return false;
1917                                         }
1918                                 }
1919                         }
1920                 }
1921         }
1922
1923         if(autocvar_g_onslaught_spawn_at_generator)
1924         if(random() <= autocvar_g_onslaught_spawn_at_generator_chance)
1925         {
1926                 float random_target = autocvar_g_onslaught_spawn_at_generator_random;
1927                 entity tmp_entity, closest_target = world;
1928                 vector spawn_loc = self.ons_deathloc;
1929
1930                 // new joining player or round reset, don't bother checking
1931                 if(spawn_loc == '0 0 0') { return false; }
1932
1933                 if(random_target) { RandomSelection_Init(); }
1934
1935                 for(tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
1936                 {
1937                         if(random_target)
1938                                 RandomSelection_Add(tmp_entity, 0, string_null, 1, 1);
1939                         else
1940                         {
1941                                 if(SAME_TEAM(tmp_entity, self))
1942                                 if(vlen(tmp_entity.origin - spawn_loc) <= vlen(closest_target.origin - spawn_loc) || closest_target == world)
1943                                         closest_target = tmp_entity;
1944                         }
1945                 }
1946
1947                 if(random_target) { closest_target = RandomSelection_chosen_ent; }
1948
1949                 if(closest_target)
1950                 {
1951                         float i;
1952                         vector loc;
1953                         float iteration_scale = 1;
1954                         for(i = 0; i < 10; ++i)
1955                         {
1956                                 iteration_scale -= i / 10;
1957                                 loc = closest_target.origin + '0 0 128' * iteration_scale;
1958                                 loc += ('0 1 0' * random()) * 256 * iteration_scale;
1959                                 tracebox(loc, PL_MIN, PL_MAX, loc, MOVE_NORMAL, self);
1960                                 if(trace_fraction == 1.0 && !trace_startsolid)
1961                                 {
1962                                         traceline(closest_target.origin, loc, MOVE_NOMONSTERS, closest_target); // double check to make sure we're not spawning outside the world
1963                                         if(trace_fraction == 1.0 && !trace_startsolid)
1964                                         {
1965                                                 setorigin(self, loc);
1966                                                 self.angles = normalize(loc - closest_target.origin) * RAD2DEG;
1967                                                 return false;
1968                                         }
1969                                 }
1970                         }
1971                 }
1972         }
1973
1974     return false;
1975 }
1976
1977 MUTATOR_HOOKFUNCTION(ons, PlayerDies)
1978 {SELFPARAM();
1979         frag_target.ons_deathloc = frag_target.origin;
1980         entity l;
1981         for(l = ons_worldgeneratorlist; l; l = l.ons_worldgeneratornext)
1982         {
1983                 l.sprite.SendFlags |= 16;
1984         }
1985         for(l = ons_worldcplist; l; l = l.ons_worldcpnext)
1986         {
1987                 l.sprite.SendFlags |= 16;
1988         }
1989
1990         if ( autocvar_g_onslaught_spawn_choose )
1991         if ( ons_Count_SelfControlPoints() > 1 )
1992                 stuffcmd(self, "qc_cmd_cl hud clickradar\n");
1993
1994         return false;
1995 }
1996
1997 MUTATOR_HOOKFUNCTION(ons, MonsterMove)
1998 {SELFPARAM();
1999         entity e = find(world, targetname, self.target);
2000         if (e != world)
2001                 self.team = e.team;
2002
2003         return false;
2004 }
2005
2006 void ons_MonsterSpawn_Delayed()
2007 {SELFPARAM();
2008         entity e, own = self.owner;
2009
2010         if(!own) { remove(self); return; }
2011
2012         if(own.targetname)
2013         {
2014                 e = find(world, target, own.targetname);
2015                 if(e != world)
2016                 {
2017                         own.team = e.team;
2018
2019                         activator = e;
2020                         own.use();
2021                 }
2022         }
2023
2024         remove(self);
2025 }
2026
2027 MUTATOR_HOOKFUNCTION(ons, MonsterSpawn)
2028 {SELFPARAM();
2029         entity e = spawn();
2030         e.owner = self;
2031         InitializeEntity(e, ons_MonsterSpawn_Delayed, INITPRIO_FINDTARGET);
2032
2033         return false;
2034 }
2035
2036 void ons_TurretSpawn_Delayed()
2037 {SELFPARAM();
2038         entity e, own = self.owner;
2039
2040         if(!own) { remove(self); return; }
2041
2042         if(own.targetname)
2043         {
2044                 e = find(world, target, own.targetname);
2045                 if(e != world)
2046                 {
2047                         own.team = e.team;
2048                         own.active = ACTIVE_NOT;
2049
2050                         activator = e;
2051                         own.use();
2052                 }
2053         }
2054
2055         remove(self);
2056 }
2057
2058 MUTATOR_HOOKFUNCTION(ons, TurretSpawn)
2059 {SELFPARAM();
2060         entity e = spawn();
2061         e.owner = self;
2062         InitializeEntity(e, ons_TurretSpawn_Delayed, INITPRIO_FINDTARGET);
2063
2064         return false;
2065 }
2066
2067 MUTATOR_HOOKFUNCTION(ons, HavocBot_ChooseRole)
2068 {SELFPARAM();
2069         havocbot_ons_reset_role(self);
2070         return true;
2071 }
2072
2073 MUTATOR_HOOKFUNCTION(ons, GetTeamCount)
2074 {
2075         // onslaught is special
2076         for(entity tmp_entity = ons_worldgeneratorlist; tmp_entity; tmp_entity = tmp_entity.ons_worldgeneratornext)
2077         {
2078                 switch(tmp_entity.team)
2079                 {
2080                         case NUM_TEAM_1: c1 = 0; break;
2081                         case NUM_TEAM_2: c2 = 0; break;
2082                         case NUM_TEAM_3: c3 = 0; break;
2083                         case NUM_TEAM_4: c4 = 0; break;
2084                 }
2085         }
2086
2087         return true;
2088 }
2089
2090 MUTATOR_HOOKFUNCTION(ons, SpectateCopy)
2091 {SELFPARAM();
2092         self.ons_roundlost = other.ons_roundlost; // make spectators see it too
2093         return false;
2094 }
2095
2096 MUTATOR_HOOKFUNCTION(ons, SV_ParseClientCommand)
2097 {SELFPARAM();
2098         if(MUTATOR_RETURNVALUE) // command was already handled?
2099                 return false;
2100
2101         if ( cmd_name == "ons_spawn" )
2102         {
2103                 vector pos = self.origin;
2104                 if(cmd_argc > 1)
2105                         pos_x = stof(argv(1));
2106                 if(cmd_argc > 2)
2107                         pos_y = stof(argv(2));
2108                 if(cmd_argc > 3)
2109                         pos_z = stof(argv(3));
2110
2111                 if ( IS_PLAYER(self) )
2112                 {
2113                         if ( !self.frozen )
2114                         {
2115                                 entity source_point = ons_Nearest_ControlPoint(self.origin, autocvar_g_onslaught_teleport_radius);
2116
2117                                 if ( !source_point && self.health > 0 )
2118                                 {
2119                                         sprint(self, "\nYou need to be next to a control point\n");
2120                                         return 1;
2121                                 }
2122
2123
2124                                 entity closest_target = ons_Nearest_ControlPoint_2D(pos, autocvar_g_onslaught_click_radius);
2125
2126                                 if ( closest_target == world )
2127                                 {
2128                                         sprint(self, "\nNo control point found\n");
2129                                         return 1;
2130                                 }
2131
2132                                 if ( self.health <= 0 )
2133                                 {
2134                                         self.ons_spawn_by = closest_target;
2135                                         self.respawn_flags = self.respawn_flags | RESPAWN_FORCE;
2136                                 }
2137                                 else
2138                                 {
2139                                         if ( source_point == closest_target )
2140                                         {
2141                                                 sprint(self, "\nTeleporting to the same point\n");
2142                                                 return 1;
2143                                         }
2144
2145                                         if ( !ons_Teleport(self,closest_target,autocvar_g_onslaught_teleport_radius,true) )
2146                                                 sprint(self, "\nUnable to teleport there\n");
2147                                 }
2148
2149                                 return 1;
2150                         }
2151
2152                         sprint(self, "\nNo teleportation for you\n");
2153                 }
2154
2155                 return 1;
2156         }
2157         return 0;
2158 }
2159
2160 MUTATOR_HOOKFUNCTION(ons, PlayerUseKey)
2161 {SELFPARAM();
2162         if(MUTATOR_RETURNVALUE || gameover) { return false; }
2163
2164         if((time > self.teleport_antispam) && (self.deadflag == DEAD_NO) && !self.vehicle)
2165         {
2166                 entity source_point = ons_Nearest_ControlPoint(self.origin, autocvar_g_onslaught_teleport_radius);
2167                 if ( source_point )
2168                 {
2169                         stuffcmd(self, "qc_cmd_cl hud clickradar\n");
2170                         return true;
2171                 }
2172         }
2173
2174         return false;
2175 }
2176
2177 MUTATOR_HOOKFUNCTION(ons, PlayHitsound)
2178 {
2179         return (frag_victim.classname == "onslaught_generator" && !frag_victim.isshielded)
2180                 || (frag_victim.classname == "onslaught_controlpoint_icon" && !frag_victim.owner.isshielded);
2181 }
2182
2183 MUTATOR_HOOKFUNCTION(ons, SendWaypoint)
2184 {
2185         if(wp_sendflags & 16)
2186         {
2187                 if(self.owner.classname == "onslaught_controlpoint")
2188                 {
2189                         entity wp_owner = self.owner;
2190                         entity e = WaypointSprite_getviewentity(wp_sendto);
2191                         if(SAME_TEAM(e, wp_owner) && wp_owner.goalentity.health >= wp_owner.goalentity.max_health) { wp_flag |= 2; }
2192                         if(!ons_ControlPoint_Attackable(wp_owner, e.team)) { wp_flag |= 2; }
2193                 }
2194                 if(self.owner.classname == "onslaught_generator")
2195                 {
2196                         entity wp_owner = self.owner;
2197                         if(wp_owner.isshielded && wp_owner.health >= wp_owner.max_health) { wp_flag |= 2; }
2198                         if(wp_owner.health <= 0) { wp_flag |= 2; }
2199                 }
2200         }
2201
2202         return false;
2203 }
2204
2205 MUTATOR_HOOKFUNCTION(ons, TurretValidateTarget)
2206 {
2207         if(substring(turret_target.classname, 0, 10) == "onslaught_") // don't attack onslaught targets, that's the player's job!
2208         {
2209                 ret_float = -3;
2210                 return true;
2211         }
2212
2213         return false;
2214 }
2215
2216 MUTATOR_HOOKFUNCTION(ons, TurretThink)
2217 {
2218         // ONS uses somewhat backwards linking.
2219         if(self.target)
2220         {
2221                 entity e = find(world, targetname, self.target);
2222                 if (e != world)
2223                         self.team = e.team;
2224         }
2225
2226         if(self.team != self.tur_head.team)
2227                 turret_respawn();
2228
2229         return false;
2230 }
2231
2232
2233 // ==========
2234 // Spawnfuncs
2235 // ==========
2236
2237 /*QUAKED spawnfunc_onslaught_link (0 .5 .8) (-16 -16 -16) (16 16 16)
2238   Link between control points.
2239
2240   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.
2241
2242 keys:
2243 "target" - first control point.
2244 "target2" - second control point.
2245  */
2246 spawnfunc(onslaught_link)
2247 {
2248         if(!g_onslaught) { remove(self); return; }
2249
2250         if (self.target == "" || self.target2 == "")
2251                 objerror("target and target2 must be set\n");
2252
2253         self.ons_worldlinknext = ons_worldlinklist; // link into ons_worldlinklist
2254         ons_worldlinklist = self;
2255
2256         InitializeEntity(self, ons_DelayedLinkSetup, INITPRIO_FINDTARGET);
2257         Net_LinkEntity(self, false, 0, ons_Link_Send);
2258 }
2259
2260 /*QUAKED spawnfunc_onslaught_controlpoint (0 .5 .8) (-32 -32 0) (32 32 128)
2261   Control point. Be sure to give this enough clearance so that the shootable part has room to exist
2262
2263   This should link to an spawnfunc_onslaught_controlpoint entity or spawnfunc_onslaught_generator entity.
2264
2265 keys:
2266 "targetname" - name that spawnfunc_onslaught_link entities will use to target this.
2267 "target" - target any entities that are tied to this control point, such as vehicles and buildable structure entities.
2268 "message" - name of this control point (should reflect the location in the map, such as "center bridge", "north tower", etc)
2269  */
2270
2271 spawnfunc(onslaught_controlpoint)
2272 {
2273         if(!g_onslaught) { remove(self); return; }
2274
2275         ons_ControlPoint_Setup(self);
2276 }
2277
2278 /*QUAKED spawnfunc_onslaught_generator (0 .5 .8) (-32 -32 -24) (32 32 64)
2279   Base generator.
2280
2281   spawnfunc_onslaught_link entities can target this.
2282
2283 keys:
2284 "team" - team that owns this generator (5 = red, 14 = blue, etc), MUST BE SET.
2285 "targetname" - name that spawnfunc_onslaught_link entities will use to target this.
2286  */
2287 spawnfunc(onslaught_generator)
2288 {
2289         if(!g_onslaught) { remove(self); return; }
2290         if(!self.team) { objerror("team must be set"); }
2291
2292         ons_GeneratorSetup(self);
2293 }
2294
2295 // scoreboard setup
2296 void ons_ScoreRules()
2297 {
2298         CheckAllowedTeams(world);
2299         ScoreRules_basics(((c4>=0) ? 4 : (c3>=0) ? 3 : 2), SFL_SORT_PRIO_PRIMARY, 0, true);
2300         ScoreInfo_SetLabel_TeamScore  (ST_ONS_CAPS,     "destroyed", SFL_SORT_PRIO_PRIMARY);
2301         ScoreInfo_SetLabel_PlayerScore(SP_ONS_CAPS,     "caps",      SFL_SORT_PRIO_SECONDARY);
2302         ScoreInfo_SetLabel_PlayerScore(SP_ONS_TAKES,    "takes",     0);
2303         ScoreRules_basics_end();
2304 }
2305
2306 void ons_DelayedInit() // Do this check with a delay so we can wait for teams to be set up
2307 {
2308         ons_ScoreRules();
2309
2310         round_handler_Spawn(Onslaught_CheckPlayers, Onslaught_CheckWinner, Onslaught_RoundStart);
2311         round_handler_Init(5, autocvar_g_onslaught_warmup, autocvar_g_onslaught_round_timelimit);
2312 }
2313
2314 void ons_Initialize()
2315 {
2316         g_onslaught = true;
2317         ons_captureshield_force = autocvar_g_onslaught_shield_force;
2318
2319         addstat(STAT_ROUNDLOST, AS_INT, ons_roundlost);
2320
2321         InitializeEntity(world, ons_DelayedInit, INITPRIO_GAMETYPE);
2322 }
2323
2324 #endif