]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/mode_onslaught.qc
minor fixed for Onslaught + sprites
[divverent/nexuiz.git] / data / qcsrc / server / mode_onslaught.qc
1 .entity sprite;
2 .string target2;
3 .float iscaptured;
4 .float islinked;
5 .float isshielded;
6 .float lasthealth;
7
8 float ons_sprite_cp_red, ons_sprite_cp_blue, ons_sprite_cp_neut;
9 float ons_sprite_gen_red, ons_sprite_gen_blue, ons_sprite_gen_shielded;
10
11 void() onslaught_updatelinks =
12 {
13         local entity l, links;
14         local float stop, t1, t2, t3, t4;
15         // first check if the game has ended
16         dprint("--- updatelinks ---\n");
17         links = findchain(classname, "onslaught_link");
18         // mark generators as being shielded and networked
19         l = findchain(classname, "onslaught_generator");
20         while (l)
21         {
22                 if (l.iscaptured)
23                         dprint(etos(l), " (generator) belongs to team ", ftos(l.team), "\n");
24                 else
25                         dprint(etos(l), " (generator) is destroyed\n");
26                 l.islinked = l.iscaptured;
27                 l.isshielded = l.iscaptured;
28                 l = l.chain;
29         }
30         // mark points as shielded and not networked
31         l = findchain(classname, "onslaught_controlpoint");
32         while (l)
33         {
34                 l.islinked = FALSE;
35                 l.isshielded = TRUE;
36                 dprint(etos(l), " (point) belongs to team ", ftos(l.team), "\n");
37                 l = l.chain;
38         }
39         // flow power outward from the generators through the network
40         l = links;
41         while (l)
42         {
43                 dprint(etos(l), " (link) connects ", etos(l.goalentity), " with ", etos(l.enemy), "\n");
44                 l = l.chain;
45         }
46         stop = FALSE;
47         while (!stop)
48         {
49                 stop = TRUE;
50                 l = links;
51                 while (l)
52                 {
53                         // if both points are captured by the same team, and only one of
54                         // them is powered, mark the other one as powered as well
55                         if (l.enemy.iscaptured && l.goalentity.iscaptured)
56                         if (l.enemy.islinked != l.goalentity.islinked)
57                         if (l.enemy.team == l.goalentity.team)
58                         {
59                                 if (!l.goalentity.islinked)
60                                 {
61                                         stop = FALSE;
62                                         l.goalentity.islinked = TRUE;
63                                         dprint(etos(l), " (link) is marking ", etos(l.goalentity), " (point) because its team matches ", etos(l.enemy), " (point)\n");
64                                 }
65                                 else if (!l.enemy.islinked)
66                                 {
67                                         stop = FALSE;
68                                         l.enemy.islinked = TRUE;
69                                         dprint(etos(l), " (link) is marking ", etos(l.enemy), " (point) because its team matches ", etos(l.goalentity), " (point)\n");
70                                 }
71                         }
72                         l = l.chain;
73                 }
74         }
75         // now that we know which points are powered we can mark their neighbors
76         // as unshielded if team differs
77         l = links;
78         while (l)
79         {
80                 if (l.goalentity.team != l.enemy.team)
81                 {
82                         if (l.goalentity.islinked)
83                         {
84                                 dprint(etos(l), " (link) is unshielding ", etos(l.enemy), " (point) because its team does not match ", etos(l.goalentity), " (point)\n");
85                                 l.enemy.isshielded = FALSE;
86                         }
87                         if (l.enemy.islinked)
88                         {
89                                 dprint(etos(l), " (link) is unshielding ", etos(l.goalentity), " (point) because its team does not match ", etos(l.enemy), " (point)\n");
90                                 l.goalentity.isshielded = FALSE;
91                         }
92                 }
93                 l = l.chain;
94         }
95         // now update the takedamage and alpha variables on generator shields
96         l = findchain(classname, "onslaught_generator");
97         while (l)
98         {
99                 if (l.isshielded)
100                 {
101                         dprint(etos(l), " (generator) is shielded\n");
102                         l.enemy.alpha = 1;
103                         l.takedamage = DAMAGE_NO;
104                         l.bot_attack = FALSE;
105                 }
106                 else
107                 {
108                         dprint(etos(l), " (generator) is not shielded\n");
109                         l.enemy.alpha = -1;
110                         l.takedamage = DAMAGE_AIM;
111                         l.bot_attack = TRUE;
112                 }
113                 l = l.chain;
114         }
115         // now update the takedamage and alpha variables on control point icons
116         l = findchain(classname, "onslaught_controlpoint");
117         while (l)
118         {
119                 if (l.isshielded)
120                 {
121                         dprint(etos(l), " (point) is shielded\n");
122                         l.enemy.alpha = 1;
123                         if (l.goalentity)
124                         {
125                                 l.goalentity.takedamage = DAMAGE_NO;
126                                 l.goalentity.bot_attack = FALSE;
127                         }
128                 }
129                 else
130                 {
131                         dprint(etos(l), " (point) is not shielded\n");
132                         l.enemy.alpha = -1;
133                         if (l.goalentity)
134                         {
135                                 l.goalentity.takedamage = DAMAGE_AIM;
136                                 l.goalentity.bot_attack = TRUE;
137                         }
138                 }
139                 l = l.chain;
140         }
141         // count generators owned by each team
142         t1 = t2 = t3 = t4 = 0;
143         l = findchain(classname, "onslaught_generator");
144         while (l)
145         {
146                 if (l.iscaptured)
147                 {
148                         if (l.team == COLOR_TEAM1) t1 = 1;
149                         if (l.team == COLOR_TEAM2) t2 = 1;
150                         if (l.team == COLOR_TEAM3) t3 = 1;
151                         if (l.team == COLOR_TEAM4) t4 = 1;
152                 }
153                 l = l.chain;
154         }
155         // see if multiple teams remain (if not, it's game over)
156         if (t1 + t2 + t3 + t4 < 2)
157                 dprint("--- game over ---\n");
158         else
159                 dprint("--- done updating links ---\n");
160 };
161
162 float onslaught_controlpoint_can_be_linked(entity cp, float t)
163 {
164         entity e;
165         // check to see if this player has a legitimate claim to capture this
166         // control point - more specifically that there is a captured path of
167         // points leading back to the team generator
168         e = findchain(classname, "onslaught_link");
169         while (e)
170         {
171                 if (e.goalentity == cp)
172                 {
173                         dprint(etos(e), " (link) connects to ", etos(e.enemy), " (point)");
174                         if (e.enemy.islinked)
175                         {
176                                 dprint(" which is linked");
177                                 if (e.enemy.team == t)
178                                 {
179                                         dprint(" and has the correct team!\n");
180                                         return 1;
181                                 }
182                                 else
183                                         dprint(" but has the wrong team\n");
184                         }
185                         else
186                                 dprint("\n");
187                 }
188                 else if (e.enemy == cp)
189                 {
190                         dprint(etos(e), " (link) connects to ", etos(e.goalentity), " (point)");
191                         if (e.goalentity.islinked)
192                         {
193                                 dprint(" which is linked");
194                                 if (e.goalentity.team == t)
195                                 {
196                                         dprint(" and has a team!\n");
197                                         return 1;
198                                 }
199                                 else
200                                         dprint(" but has the wrong team\n");
201                         }
202                         else
203                                 dprint("\n");
204                 }
205                 e = e.chain;
206         }
207         return 0;
208 }
209
210 float onslaught_controlpoint_attackable(entity cp, float t)
211 // -1: SAME TEAM!
212 // 0:  off limits
213 // 1:  attack it
214 // 2:  touch it
215 {
216         if(cp.isshielded)
217         {
218                 return 0;
219         }
220         else if(cp.goalentity)
221         {
222                 // if there's already an icon built, nothing happens
223                 if(cp.team == t)
224                         return -1;
225                 if(onslaught_controlpoint_can_be_linked(cp, t))
226                         return 1;
227         }
228         else
229         {
230                 // free point
231                 if(onslaught_controlpoint_can_be_linked(cp, t))
232                         return 2;
233         }
234         return 0;
235 }
236
237 void() onslaught_generator_think =
238 {
239         local float d;
240         local entity e;
241         self.nextthink = ceil(time + 1);
242         if (cvar("timelimit"))
243         if (time > cvar("timelimit") * 60 - 60)
244         {
245                 // self.max_health / 300 gives 5 minutes of overtime.
246                 // control points reduce the overtime duration.
247                 sound(self, CHAN_AUTO, "sound/onslaught/generator_decay.wav", 1, ATTN_NORM);
248                 d = 1;
249                 e = findchain(classname, "onslaught_controlpoint");
250                 while (e)
251                 {
252                         if (e.team != self.team)
253                         if (e.islinked)
254                                 d = d + 1;
255                         e = e.chain;
256                 }
257                 d = d * self.max_health / 300;
258                 Damage(self, self, self, d, DEATH_HURTTRIGGER, self.origin, '0 0 0');
259         }
260 };
261
262 void() onslaught_generator_deaththink =
263 {
264         local vector org;
265         if (self.count > 0)
266         {
267                 self.nextthink = time + 0.1;
268                 self.count = self.count - 1;
269                 org = randompos(self.origin + self.mins + '8 8 8', self.origin + self.maxs + '-8 -8 -8');
270                 pointparticles(particleeffectnum("onslaught_generator_smallexplosion"), org, '0 0 0', 1);
271                 sound(self, CHAN_AUTO, "sound/weapons/grenade_impact.wav", 1, ATTN_NORM);
272         }
273         else
274         {
275                 org = self.origin;
276                 pointparticles(particleeffectnum("onslaught_generator_finalexplosion"), org, '0 0 0', 1);
277                 sound(self, CHAN_AUTO, "sound/weapons/rocket_impact.wav", 1, ATTN_NORM);
278         }
279 };
280
281 void(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) onslaught_generator_damage =
282 {
283         if (damage <= 0)
284                 return;
285         if (attacker != self)
286         {
287                 if (self.isshielded)
288                 {
289                         // this is protected by a shield, so ignore the damage
290                         if (time > self.pain_finished)
291                         if (attacker.classname == "player")
292                         {
293                                 play2(attacker, "sound/onslaught/damageblockedbyshield.wav");
294                                 self.pain_finished = time + 1;
295                         }
296                         return;
297                 }
298                 if (time > self.pain_finished)
299                 {
300                         self.pain_finished = time + 5;
301                         bprint(ColoredTeamName(self.team), " generator under attack!\n");
302                         play2team(self.team, "sound/onslaught/generator_underattack.wav");
303                 }
304         }
305         self.health = self.health - damage;
306         // choose an animation frame based on health
307         self.frame = 10 * bound(0, (1 - self.health / self.max_health), 1);
308         // see if the generator is still functional, or dying
309         if (self.health > 0)
310         {
311                 float h, lh;
312                 lh = ceil(self.lasthealth / 100) * 100;
313                 h = ceil(self.health / 100) * 100;
314                 if(lh != h)
315                         bprint(ColoredTeamName(self.team), " generator has less than ", ftos(h), " health remaining\n");
316                 self.lasthealth = self.health;
317         }
318         else
319         {
320                 if (attacker == self)
321                         bprint(ColoredTeamName(self.team), " generator spontaneously exploded due to overtime!\n");
322                 else
323                         bprint(ColoredTeamName(self.team), " generator destroyed by ", ColoredTeamName(attacker.team), "!\n");
324                 self.iscaptured = FALSE;
325                 self.islinked = FALSE;
326                 self.isshielded = FALSE;
327                 self.takedamage = DAMAGE_NO; // can't be hurt anymore
328                 self.event_damage = SUB_Null; // won't do anything if hurt
329                 self.count = 30; // 30 explosions
330                 self.think = onslaught_generator_deaththink; // explosion sequence
331                 self.nextthink = time; // start exploding immediately
332                 self.think(); // do the first explosion now
333                 onslaught_updatelinks();
334         }
335 };
336
337 // update links after a delay
338 void() onslaught_generator_delayed =
339 {
340         onslaught_updatelinks();
341         // now begin normal thinking
342         self.think = onslaught_generator_think;
343         self.nextthink = time;
344 };
345
346 float onslaught_generator_waypointsprite_for_player(entity e)
347 {
348         if(e.classname == "player")
349                 if(e.team == self.owner.team)
350                 {
351                         if(self.owner.team == COLOR_TEAM1)
352                                 return ons_sprite_gen_red;
353                         else if(self.owner.team == COLOR_TEAM2)
354                                 return ons_sprite_gen_blue;
355                 }
356         if(self.owner.isshielded)
357                 return ons_sprite_gen_shielded;
358         if(self.owner.team == COLOR_TEAM1)
359                 return ons_sprite_gen_red;
360         else if(self.owner.team == COLOR_TEAM2)
361                 return ons_sprite_gen_blue;
362         return 0;
363 }
364
365 float onslaught_controlpoint_waypointsprite_for_player(entity e)
366 {
367         float a;
368         if(e.classname == "player")
369         {
370                 a = onslaught_controlpoint_attackable(self.owner, e.team);
371                 if(a == -1 || a == 1) // own point, or fire at it
372                 {
373                         if(self.owner.team == COLOR_TEAM1)
374                                 return ons_sprite_cp_red;
375                         else if(self.owner.team == COLOR_TEAM2)
376                                 return ons_sprite_cp_blue;
377                 }
378                 else if(a == 2) // touch it
379                         return ons_sprite_cp_neut;
380         }
381         else
382         {
383                 if(self.owner.team == COLOR_TEAM1)
384                         return ons_sprite_cp_red;
385                 else if(self.owner.team == COLOR_TEAM2)
386                         return ons_sprite_cp_blue;
387                 else
388                         return ons_sprite_cp_neut;
389         }
390         return 0;
391 }
392
393 /*QUAKED onslaught_generator (0 .5 .8) (-32 -32 -24) (32 32 64)
394 Base generator.
395
396 onslaught_link entities can target this.
397
398 keys:
399 "team" - team that owns this generator (5 = red, 14 = blue, etc), MUST BE SET.
400 "targetname" - name that onslaught_link entities will use to target this.
401 */
402 void() onslaught_generator =
403 {
404         if (!g_onslaught)
405         {
406                 remove(self);
407                 return;
408         }
409
410         if(!ons_sprite_cp_blue)
411         {
412                 precache_model("models/sprites/ons-cp-blue.sp2");
413                 setmodel(self, "models/sprites/ons-cp-blue.sp2");
414                 ons_sprite_cp_blue = self.modelindex;
415                 precache_model("models/sprites/ons-cp-red.sp2");
416                 setmodel(self, "models/sprites/ons-cp-red.sp2");
417                 ons_sprite_cp_red = self.modelindex;
418                 precache_model("models/sprites/ons-cp-neut.sp2");
419                 setmodel(self, "models/sprites/ons-cp-neut.sp2");
420                 ons_sprite_cp_neut = self.modelindex;
421                 precache_model("models/sprites/ons-gen-blue.sp2");
422                 setmodel(self, "models/sprites/ons-gen-blue.sp2");
423                 ons_sprite_gen_blue = self.modelindex;
424                 precache_model("models/sprites/ons-gen-red.sp2");
425                 setmodel(self, "models/sprites/ons-gen-red.sp2");
426                 ons_sprite_gen_red = self.modelindex;
427                 precache_model("models/sprites/ons-gen-shielded.sp2");
428                 setmodel(self, "models/sprites/ons-gen-shielded.sp2");
429                 ons_sprite_gen_shielded = self.modelindex;
430         }
431
432         local entity e;
433         precache_model("models/onslaught/generator.md3");
434         precache_model("models/onslaught/generator_shield.md3");
435         precache_sound("sound/onslaught/generator_decay.wav");
436         precache_sound("sound/weapons/grenade_impact.wav");
437         precache_sound("sound/weapons/rocket_impact.wav");
438         precache_sound("sound/onslaught/generator_underattack.wav");
439         if (!self.team)
440                 objerror("team must be set");
441         self.colormap = 1024 + (self.team - 1) * 17;
442         self.solid = SOLID_BSP;
443         self.movetype = MOVETYPE_NONE;
444         self.lasthealth = self.max_health = self.health = 3000;
445         setmodel(self, "models/onslaught/generator.md3");
446         //setsize(self, '-32 -32 -24', '32 32 64');
447         setorigin(self, self.origin);
448         self.takedamage = DAMAGE_AIM;
449         self.bot_attack = TRUE;
450         self.event_damage = onslaught_generator_damage;
451         self.iscaptured = TRUE;
452         self.islinked = TRUE;
453         self.isshielded = TRUE;
454         // spawn shield model which indicates whether this can be damaged
455         self.enemy = e = spawn();
456         e.solid = SOLID_NOT;
457         e.movetype = MOVETYPE_NONE;
458         e.effects = EF_ADDITIVE;
459         setmodel(e, "models/onslaught/generator_shield.md3");
460         //setsize(e, '-32 -32 0', '32 32 128');
461         setorigin(e, self.origin);
462         e.colormap = self.colormap;
463         e.team = self.team;
464         self.think = onslaught_generator_delayed;
465         self.nextthink = time + 0.2;
466
467         WaypointSprite_SpawnFixed(string_null, e.origin + '0 0 1' * e.maxs_z, self, sprite);
468         self.sprite.waypointsprite_for_player = onslaught_generator_waypointsprite_for_player;
469 };
470
471 void(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) onslaught_controlpoint_icon_damage =
472 {
473         entity oself;
474         if (damage <= 0)
475                 return;
476         if (self.owner.isshielded)
477         {
478                 // this is protected by a shield, so ignore the damage
479                 if (time > self.pain_finished)
480                 if (attacker.classname == "player")
481                 {
482                         play2(attacker, "sound/onslaught/damageblockedbyshield.wav");
483                         self.pain_finished = time + 1;
484                 }
485                 return;
486         }
487         if (time > self.pain_finished)
488         if (attacker.classname == "player")
489         {
490                 play2team(self.team, "sound/onslaught/controlpoint_underattack.wav");
491                 self.pain_finished = time + 5;
492         }
493         self.health = self.health - damage;
494         self.alpha = self.health / self.max_health;
495         self.pain_finished = time + 1;
496         // colormod flash when shot
497         self.colormod = '2 2 2';
498         if (self.health < 0)
499         {
500                 sound(self, CHAN_AUTO, "sound/weapons/grenade_impact.wav", 1, ATTN_NORM);
501                 pointparticles(particleeffectnum("onslaught_controlpoint_explosion"), self.origin, '0 0 0', 1);
502                 bprint(ColoredTeamName(self.team), " ", self.message, " control point destroyed by ", ColoredTeamName(attacker.team), "\n");
503                 self.owner.goalentity = world;
504                 self.owner.islinked = FALSE;
505                 self.owner.iscaptured = FALSE;
506                 self.owner.team = 0;
507                 self.owner.colormap = 1024;
508                 onslaught_updatelinks();
509
510                 // Use targets now (somebody make sure this is in the right place..)
511                 oself = self;
512                 self = self.owner;
513                 activator = self.owner;
514                 SUB_UseTargets ();
515                 self = oself;
516
517                 remove(self);
518         }
519 };
520
521 void() onslaught_controlpoint_icon_think =
522 {
523         self.nextthink = time + 0.1;
524         if (time > self.pain_finished + 1)
525         {
526                 self.health = self.health + self.count;
527                 if (self.health >= self.max_health)
528                         self.health = self.max_health;
529         }
530         self.alpha = self.health / self.max_health;
531         // colormod flash when shot
532         self.colormod = '1 1 1' * (2 - bound(0, (self.pain_finished - time) / 10, 1));
533 };
534
535 void() onslaught_controlpoint_icon_buildthink =
536 {
537         local entity oself;
538
539         self.nextthink = time + 0.1;
540         self.health = self.health + self.count;
541         if (self.health >= self.max_health)
542         {
543                 self.health = self.max_health;
544                 self.count = self.count * 0.2; // slow repair rate from now on
545                 self.think = onslaught_controlpoint_icon_think;
546                 sound(self, CHAN_BODY, "sound/onslaught/controlpoint_built.wav", 1, ATTN_NORM);
547                 bprint(ColoredTeamName(self.team), " captured ", self.owner.message, " control point\n");
548                 self.owner.iscaptured = TRUE;
549                 onslaught_updatelinks();
550
551                 // Use targets now (somebody make sure this is in the right place..)
552                 oself = self;
553                 self = self.owner;
554                 activator = self;
555                 SUB_UseTargets ();
556                 self = oself;
557         }
558         self.alpha = self.health / self.max_health;
559         // colormod flash when shot
560         self.colormod = '1 1 1' * (2 - bound(0, (self.pain_finished - time) / 10, 1));
561 };
562
563 void() onslaught_controlpoint_touch =
564 {
565         local entity e;
566         if (other.classname != "player")
567                 return;
568         if(onslaught_controlpoint_attackable(self, other.team) != 2)
569                 return;
570         // we've verified that this player has a legitimate claim to this point,
571         // so start building the captured point icon (which only captures this
572         // point if it successfully builds without being destroyed first)
573         self.goalentity = e = spawn();
574         e.owner = self;
575         e.max_health = 1000;
576         e.health = e.max_health * 0.1;
577         e.alpha = e.health / e.max_health;
578         e.solid = SOLID_BBOX;
579         e.movetype = MOVETYPE_NONE;
580         setmodel(e, "models/onslaught/controlpoint_icon.md3");
581         setsize(e, '-32 -32 -32', '32 32 32');
582         setorigin(e, self.origin + '0 0 96');
583         e.takedamage = DAMAGE_AIM;
584         e.bot_attack = TRUE;
585         e.event_damage = onslaught_controlpoint_icon_damage;
586         e.team = other.team;
587         e.colormap = 1024 + (e.team - 1) * 17;
588         e.think = onslaught_controlpoint_icon_buildthink;
589         e.nextthink = time + 0.1;
590         e.count = e.max_health / 50; // how long it takes to build
591         sound(e, CHAN_BODY, "sound/onslaught/controlpoint_build.wav", 1, ATTN_NORM);
592         self.team = e.team;
593         self.colormap = e.colormap;
594 };
595
596 /*QUAKED onslaught_controlpoint (0 .5 .8) (-32 -32 0) (32 32 128)
597 Control point.  Be sure to give this enough clearance so that the shootable part has room to exist
598
599 This should link to an onslaught_controlpoint entity or onslaught_generator entity.
600
601 keys:
602 "targetname" - name that onslaught_link entities will use to target this.
603 "target" - target any entities that are tied to this control point, such as vehicles and buildable structure entities.
604 "message" - name of this control point (should reflect the location in the map, such as "center bridge", "north tower", etc)
605 */
606 void() onslaught_controlpoint =
607 {
608         local entity e;
609         if (!g_onslaught)
610         {
611                 remove(self);
612                 return;
613         }
614         precache_model("models/onslaught/controlpoint_pad.md3");
615         precache_model("models/onslaught/controlpoint_shield.md3");
616         precache_model("models/onslaught/controlpoint_icon.md3");
617         precache_sound("sound/onslaught/controlpoint_build.wav");
618         precache_sound("sound/onslaught/controlpoint_built.wav");
619         precache_sound("sound/weapons/grenade_impact.wav");
620         precache_sound("sound/onslaught/damageblockedbyshield.wav");
621         precache_sound("sound/onslaught/controlpoint_underattack.wav");
622         self.solid = SOLID_BSP;
623         self.movetype = MOVETYPE_NONE;
624         setmodel(self, "models/onslaught/controlpoint_pad.md3");
625         //setsize(self, '-32 -32 0', '32 32 8');
626         setorigin(self, self.origin);
627         self.touch = onslaught_controlpoint_touch;
628         self.team = 0;
629         self.colormap = 1024;
630         self.iscaptured = FALSE;
631         self.islinked = FALSE;
632         self.isshielded = TRUE;
633         // spawn shield model which indicates whether this can be damaged
634         self.enemy = e = spawn();
635         e.solid = SOLID_NOT;
636         e.movetype = MOVETYPE_NONE;
637         e.effects = EF_ADDITIVE;
638         setmodel(e, "models/onslaught/controlpoint_shield.md3");
639         //setsize(e, '-32 -32 0', '32 32 128');
640         setorigin(e, self.origin);
641         e.colormap = self.colormap;
642         onslaught_updatelinks();
643
644         WaypointSprite_SpawnFixed(string_null, e.origin + '0 0 1' * e.maxs_z, self, sprite);
645         self.sprite.waypointsprite_for_player = onslaught_controlpoint_waypointsprite_for_player;
646 };
647
648 void() onslaught_link_delayed =
649 {
650         self.goalentity = find(world, targetname, self.target);
651         self.enemy = find(world, targetname, self.target2);
652         if (!self.goalentity)
653                 objerror("can not find target\n");
654         if (!self.enemy)
655                 objerror("can not find target2\n");
656         dprint(etos(self.goalentity), " linked with ", etos(self.enemy), "\n");
657 }
658
659 /*QUAKED onslaught_link (0 .5 .8) (-16 -16 -16) (16 16 16)
660 Link between control points.
661
662 This entity targets two different onslaught_controlpoint or onslaught_generator entities, and suppresses shielding on both if they are owned by different teams.
663
664 keys:
665 "target" - first control point.
666 "target2" - second control point.
667 */
668 void() onslaught_link =
669 {
670         if (!g_onslaught)
671         {
672                 remove(self);
673                 return;
674         }
675         if (self.target == "" || self.target2 == "")
676                 objerror("target and target2 must be set\n");
677         self.think = onslaught_link_delayed;
678         self.nextthink = time + 0.1;
679 };