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