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