]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/mode_onslaught.qc
fix two minor bugs
[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                 {
324                         string t;
325                         t = ColoredTeamName(attacker.team);
326                         bprint(ColoredTeamName(self.team), " generator destroyed by ", t, "!\n");
327                 }
328                 self.iscaptured = FALSE;
329                 self.islinked = FALSE;
330                 self.isshielded = FALSE;
331                 self.takedamage = DAMAGE_NO; // can't be hurt anymore
332                 self.event_damage = SUB_Null; // won't do anything if hurt
333                 self.count = 30; // 30 explosions
334                 self.think = onslaught_generator_deaththink; // explosion sequence
335                 self.nextthink = time; // start exploding immediately
336                 self.think(); // do the first explosion now
337                 onslaught_updatelinks();
338         }
339 };
340
341 // update links after a delay
342 void() onslaught_generator_delayed =
343 {
344         onslaught_updatelinks();
345         // now begin normal thinking
346         self.think = onslaught_generator_think;
347         self.nextthink = time;
348 };
349
350 float onslaught_generator_waypointsprite_for_player(entity e)
351 {
352         if(e.classname == "player")
353                 if(e.team == self.owner.team)
354                 {
355                         if(self.owner.team == COLOR_TEAM1)
356                                 return ons_sprite_gen_red;
357                         else if(self.owner.team == COLOR_TEAM2)
358                                 return ons_sprite_gen_blue;
359                 }
360         if(self.owner.isshielded)
361                 return ons_sprite_gen_shielded;
362         if(self.owner.team == COLOR_TEAM1)
363                 return ons_sprite_gen_red;
364         else if(self.owner.team == COLOR_TEAM2)
365                 return ons_sprite_gen_blue;
366         return 0;
367 }
368
369 float onslaught_controlpoint_waypointsprite_for_player(entity e)
370 {
371         float a;
372         if(e.classname == "player")
373         {
374                 a = onslaught_controlpoint_attackable(self.owner, e.team);
375                 if(self.owner.team == e.team || a == -1 || a == 1) // own point, or fire at it
376                 {
377                         if(self.owner.team == COLOR_TEAM1)
378                                 return ons_sprite_cp_red;
379                         else if(self.owner.team == COLOR_TEAM2)
380                                 return ons_sprite_cp_blue;
381                 }
382                 else if(a == 2) // touch it
383                         return ons_sprite_cp_neut;
384         }
385         else
386         {
387                 if(self.owner.team == COLOR_TEAM1)
388                         return ons_sprite_cp_red;
389                 else if(self.owner.team == COLOR_TEAM2)
390                         return ons_sprite_cp_blue;
391                 else
392                         return ons_sprite_cp_neut;
393         }
394         return 0;
395 }
396
397 /*QUAKED onslaught_generator (0 .5 .8) (-32 -32 -24) (32 32 64)
398 Base generator.
399
400 onslaught_link entities can target this.
401
402 keys:
403 "team" - team that owns this generator (5 = red, 14 = blue, etc), MUST BE SET.
404 "targetname" - name that onslaught_link entities will use to target this.
405 */
406 void() onslaught_generator =
407 {
408         if (!g_onslaught)
409         {
410                 remove(self);
411                 return;
412         }
413
414         if(!ons_sprite_cp_blue)
415         {
416                 precache_model("models/sprites/ons-cp-blue.sp2");
417                 setmodel(self, "models/sprites/ons-cp-blue.sp2");
418                 ons_sprite_cp_blue = self.modelindex;
419                 precache_model("models/sprites/ons-cp-red.sp2");
420                 setmodel(self, "models/sprites/ons-cp-red.sp2");
421                 ons_sprite_cp_red = self.modelindex;
422                 precache_model("models/sprites/ons-cp-neut.sp2");
423                 setmodel(self, "models/sprites/ons-cp-neut.sp2");
424                 ons_sprite_cp_neut = self.modelindex;
425                 precache_model("models/sprites/ons-gen-blue.sp2");
426                 setmodel(self, "models/sprites/ons-gen-blue.sp2");
427                 ons_sprite_gen_blue = self.modelindex;
428                 precache_model("models/sprites/ons-gen-red.sp2");
429                 setmodel(self, "models/sprites/ons-gen-red.sp2");
430                 ons_sprite_gen_red = self.modelindex;
431                 precache_model("models/sprites/ons-gen-shielded.sp2");
432                 setmodel(self, "models/sprites/ons-gen-shielded.sp2");
433                 ons_sprite_gen_shielded = self.modelindex;
434         }
435
436         local entity e;
437         precache_model("models/onslaught/generator.md3");
438         precache_model("models/onslaught/generator_shield.md3");
439         precache_sound("sound/onslaught/generator_decay.wav");
440         precache_sound("sound/weapons/grenade_impact.wav");
441         precache_sound("sound/weapons/rocket_impact.wav");
442         precache_sound("sound/onslaught/generator_underattack.wav");
443         if (!self.team)
444                 objerror("team must be set");
445         self.colormap = 1024 + (self.team - 1) * 17;
446         self.solid = SOLID_BSP;
447         self.movetype = MOVETYPE_NONE;
448         self.lasthealth = self.max_health = self.health = 3000;
449         setmodel(self, "models/onslaught/generator.md3");
450         //setsize(self, '-32 -32 -24', '32 32 64');
451         setorigin(self, self.origin);
452         self.takedamage = DAMAGE_AIM;
453         self.bot_attack = TRUE;
454         self.event_damage = onslaught_generator_damage;
455         self.iscaptured = TRUE;
456         self.islinked = TRUE;
457         self.isshielded = TRUE;
458         // spawn shield model which indicates whether this can be damaged
459         self.enemy = e = spawn();
460         e.solid = SOLID_NOT;
461         e.movetype = MOVETYPE_NONE;
462         e.effects = EF_ADDITIVE;
463         setmodel(e, "models/onslaught/generator_shield.md3");
464         //setsize(e, '-32 -32 0', '32 32 128');
465         setorigin(e, self.origin);
466         e.colormap = self.colormap;
467         e.team = self.team;
468         self.think = onslaught_generator_delayed;
469         self.nextthink = time + 0.2;
470
471         WaypointSprite_SpawnFixed(string_null, e.origin + '0 0 1' * e.maxs_z, self, sprite);
472         self.sprite.waypointsprite_for_player = onslaught_generator_waypointsprite_for_player;
473 };
474
475 void(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) onslaught_controlpoint_icon_damage =
476 {
477         entity oself;
478         if (damage <= 0)
479                 return;
480         if (self.owner.isshielded)
481         {
482                 // this is protected by a shield, so ignore the damage
483                 if (time > self.pain_finished)
484                 if (attacker.classname == "player")
485                 {
486                         play2(attacker, "sound/onslaught/damageblockedbyshield.wav");
487                         self.pain_finished = time + 1;
488                 }
489                 return;
490         }
491         if (time > self.pain_finished)
492         if (attacker.classname == "player")
493         {
494                 play2team(self.team, "sound/onslaught/controlpoint_underattack.wav");
495                 self.pain_finished = time + 5;
496         }
497         self.health = self.health - damage;
498         self.alpha = self.health / self.max_health;
499         self.pain_finished = time + 1;
500         // colormod flash when shot
501         self.colormod = '2 2 2';
502         if (self.health < 0)
503         {
504                 sound(self, CHAN_AUTO, "sound/weapons/grenade_impact.wav", 1, ATTN_NORM);
505                 pointparticles(particleeffectnum("onslaught_controlpoint_explosion"), self.origin, '0 0 0', 1);
506                 bprint(ColoredTeamName(self.team), " ", self.message, " control point destroyed by ", ColoredTeamName(attacker.team), "\n");
507                 self.owner.goalentity = world;
508                 self.owner.islinked = FALSE;
509                 self.owner.iscaptured = FALSE;
510                 self.owner.team = 0;
511                 self.owner.colormap = 1024;
512                 onslaught_updatelinks();
513
514                 // Use targets now (somebody make sure this is in the right place..)
515                 oself = self;
516                 self = self.owner;
517                 activator = self.owner;
518                 SUB_UseTargets ();
519                 self = oself;
520
521                 remove(self);
522         }
523 };
524
525 void() onslaught_controlpoint_icon_think =
526 {
527         self.nextthink = time + 0.1;
528         if (time > self.pain_finished + 1)
529         {
530                 self.health = self.health + self.count;
531                 if (self.health >= self.max_health)
532                         self.health = self.max_health;
533         }
534         self.alpha = self.health / self.max_health;
535         // colormod flash when shot
536         self.colormod = '1 1 1' * (2 - bound(0, (self.pain_finished - time) / 10, 1));
537 };
538
539 void() onslaught_controlpoint_icon_buildthink =
540 {
541         local entity oself;
542
543         self.nextthink = time + 0.1;
544         self.health = self.health + self.count;
545         if (self.health >= self.max_health)
546         {
547                 self.health = self.max_health;
548                 self.count = self.count * 0.2; // slow repair rate from now on
549                 self.think = onslaught_controlpoint_icon_think;
550                 sound(self, CHAN_BODY, "sound/onslaught/controlpoint_built.wav", 1, ATTN_NORM);
551                 bprint(ColoredTeamName(self.team), " captured ", self.owner.message, " control point\n");
552                 self.owner.iscaptured = TRUE;
553                 onslaught_updatelinks();
554
555                 // Use targets now (somebody make sure this is in the right place..)
556                 oself = self;
557                 self = self.owner;
558                 activator = self;
559                 SUB_UseTargets ();
560                 self = oself;
561         }
562         self.alpha = self.health / self.max_health;
563         // colormod flash when shot
564         self.colormod = '1 1 1' * (2 - bound(0, (self.pain_finished - time) / 10, 1));
565 };
566
567 void() onslaught_controlpoint_touch =
568 {
569         local entity e;
570         if (other.classname != "player")
571                 return;
572         if(onslaught_controlpoint_attackable(self, other.team) != 2)
573                 return;
574         // we've verified that this player has a legitimate claim to this point,
575         // so start building the captured point icon (which only captures this
576         // point if it successfully builds without being destroyed first)
577         self.goalentity = e = spawn();
578         e.owner = self;
579         e.max_health = 1000;
580         e.health = e.max_health * 0.1;
581         e.alpha = e.health / e.max_health;
582         e.solid = SOLID_BBOX;
583         e.movetype = MOVETYPE_NONE;
584         setmodel(e, "models/onslaught/controlpoint_icon.md3");
585         setsize(e, '-32 -32 -32', '32 32 32');
586         setorigin(e, self.origin + '0 0 96');
587         e.takedamage = DAMAGE_AIM;
588         e.bot_attack = TRUE;
589         e.event_damage = onslaught_controlpoint_icon_damage;
590         e.team = other.team;
591         e.colormap = 1024 + (e.team - 1) * 17;
592         e.think = onslaught_controlpoint_icon_buildthink;
593         e.nextthink = time + 0.1;
594         e.count = e.max_health / 50; // how long it takes to build
595         sound(e, CHAN_BODY, "sound/onslaught/controlpoint_build.wav", 1, ATTN_NORM);
596         self.team = e.team;
597         self.colormap = e.colormap;
598 };
599
600 /*QUAKED onslaught_controlpoint (0 .5 .8) (-32 -32 0) (32 32 128)
601 Control point.  Be sure to give this enough clearance so that the shootable part has room to exist
602
603 This should link to an onslaught_controlpoint entity or onslaught_generator entity.
604
605 keys:
606 "targetname" - name that onslaught_link entities will use to target this.
607 "target" - target any entities that are tied to this control point, such as vehicles and buildable structure entities.
608 "message" - name of this control point (should reflect the location in the map, such as "center bridge", "north tower", etc)
609 */
610 void() onslaught_controlpoint =
611 {
612         local entity e;
613         if (!g_onslaught)
614         {
615                 remove(self);
616                 return;
617         }
618         precache_model("models/onslaught/controlpoint_pad.md3");
619         precache_model("models/onslaught/controlpoint_shield.md3");
620         precache_model("models/onslaught/controlpoint_icon.md3");
621         precache_sound("sound/onslaught/controlpoint_build.wav");
622         precache_sound("sound/onslaught/controlpoint_built.wav");
623         precache_sound("sound/weapons/grenade_impact.wav");
624         precache_sound("sound/onslaught/damageblockedbyshield.wav");
625         precache_sound("sound/onslaught/controlpoint_underattack.wav");
626         self.solid = SOLID_BSP;
627         self.movetype = MOVETYPE_NONE;
628         setmodel(self, "models/onslaught/controlpoint_pad.md3");
629         //setsize(self, '-32 -32 0', '32 32 8');
630         setorigin(self, self.origin);
631         self.touch = onslaught_controlpoint_touch;
632         self.team = 0;
633         self.colormap = 1024;
634         self.iscaptured = FALSE;
635         self.islinked = FALSE;
636         self.isshielded = TRUE;
637         // spawn shield model which indicates whether this can be damaged
638         self.enemy = e = spawn();
639         e.solid = SOLID_NOT;
640         e.movetype = MOVETYPE_NONE;
641         e.effects = EF_ADDITIVE;
642         setmodel(e, "models/onslaught/controlpoint_shield.md3");
643         //setsize(e, '-32 -32 0', '32 32 128');
644         setorigin(e, self.origin);
645         e.colormap = self.colormap;
646         onslaught_updatelinks();
647
648         WaypointSprite_SpawnFixed(string_null, e.origin + '0 0 1' * e.maxs_z, self, sprite);
649         self.sprite.waypointsprite_for_player = onslaught_controlpoint_waypointsprite_for_player;
650 };
651
652 void() onslaught_link_delayed =
653 {
654         self.goalentity = find(world, targetname, self.target);
655         self.enemy = find(world, targetname, self.target2);
656         if (!self.goalentity)
657                 objerror("can not find target\n");
658         if (!self.enemy)
659                 objerror("can not find target2\n");
660         dprint(etos(self.goalentity), " linked with ", etos(self.enemy), "\n");
661 }
662
663 /*QUAKED onslaught_link (0 .5 .8) (-16 -16 -16) (16 16 16)
664 Link between control points.
665
666 This entity targets two different onslaught_controlpoint or onslaught_generator entities, and suppresses shielding on both if they are owned by different teams.
667
668 keys:
669 "target" - first control point.
670 "target2" - second control point.
671 */
672 void() onslaught_link =
673 {
674         if (!g_onslaught)
675         {
676                 remove(self);
677                 return;
678         }
679         if (self.target == "" || self.target2 == "")
680                 objerror("target and target2 must be set\n");
681         self.think = onslaught_link_delayed;
682         self.nextthink = time + 0.1;
683 };