]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/g_triggers.qc
refactored model entity code to make it more readable
[divverent/nexuiz.git] / data / qcsrc / server / g_triggers.qc
1 void SUB_DontUseTargets()
2 {
3 }
4
5
6 void() SUB_UseTargets;
7
8 void DelayThink()
9 {
10         activator = self.enemy;
11         SUB_UseTargets ();
12         remove(self);
13 };
14
15 /*
16 ==============================
17 SUB_UseTargets
18
19 the global "activator" should be set to the entity that initiated the firing.
20
21 If self.delay is set, a DelayedUse entity will be created that will actually
22 do the SUB_UseTargets after that many seconds have passed.
23
24 Centerprints any self.message to the activator.
25
26 Removes all entities with a targetname that match self.killtarget,
27 and removes them, so some events can remove other triggers.
28
29 Search for (string)targetname in all entities that
30 match (string)self.target and call their .use function
31
32 ==============================
33 */
34 void SUB_UseTargets()
35 {
36         local entity t, stemp, otemp, act;
37         string s;
38         float i;
39
40 //
41 // check for a delay
42 //
43         if (self.delay)
44         {
45         // create a temp object to fire at a later time
46                 t = spawn();
47                 t.classname = "DelayedUse";
48                 t.nextthink = time + self.delay;
49                 t.think = DelayThink;
50                 t.enemy = activator;
51                 t.message = self.message;
52                 t.killtarget = self.killtarget;
53                 t.target = self.target;
54                 return;
55         }
56
57
58 //
59 // print the message
60 //
61         if (activator.classname == "player" && self.message != "")
62         {
63                 if(clienttype(activator) == CLIENTTYPE_REAL)
64                 {
65                         centerprint (activator, self.message);
66                         if (!self.noise)
67                                 play2(activator, "misc/talk.wav");
68                 }
69         }
70
71 //
72 // kill the killtagets
73 //
74         s = self.killtarget;
75         if (s != "")
76         {
77                 for(t = world; (t = find(t, targetname, s)); )
78                         remove(t);
79         }
80
81 //
82 // fire targets
83 //
84         act = activator;
85         stemp = self;
86         otemp = other;
87
88         for(i = 0; i < 4; ++i)
89         {
90                 switch(i)
91                 {
92                         default:
93                         case 0: s = stemp.target; break;
94                         case 1: s = stemp.target2; break;
95                         case 2: s = stemp.target3; break;
96                         case 3: s = stemp.target4; break;
97                 }
98                 if (s != "")
99                 {
100                         for(t = world; (t = find(t, targetname, s)); )
101                         if(t.use)
102                         {
103                                 //print(stemp.classname, " ", stemp.targetname, " -> ", t.classname, " ", t.targetname, "\n");
104                                 self = t;
105                                 other = stemp;
106                                 activator = act;
107                                 self.use();
108                         }
109                 }
110         }
111
112         activator = act;
113         self = stemp;
114         other = otemp;
115 };
116
117
118 //=============================================================================
119
120 float   SPAWNFLAG_NOMESSAGE = 1;
121 float   SPAWNFLAG_NOTOUCH = 1;
122
123 // the wait time has passed, so set back up for another activation
124 void multi_wait()
125 {
126         if (self.max_health)
127         {
128                 self.health = self.max_health;
129                 self.takedamage = DAMAGE_YES;
130                 self.solid = SOLID_BBOX;
131         }
132 };
133
134
135 // the trigger was just touched/killed/used
136 // self.enemy should be set to the activator so it can be held through a delay
137 // so wait for the delay time before firing
138 void multi_trigger()
139 {
140         if (self.nextthink > time)
141         {
142                 return;         // allready been triggered
143         }
144
145         if (self.classname == "trigger_secret")
146         {
147                 if (self.enemy.classname != "player")
148                         return;
149                 found_secrets = found_secrets + 1;
150                 WriteByte (MSG_ALL, SVC_FOUNDSECRET);
151         }
152
153         if (self.noise)
154                 sound (self.enemy, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NORM);
155
156 // don't trigger again until reset
157         self.takedamage = DAMAGE_NO;
158
159         activator = self.enemy;
160
161         SUB_UseTargets();
162
163         if (self.wait > 0)
164         {
165                 self.think = multi_wait;
166                 self.nextthink = time + self.wait;
167         }
168         else
169         {       // we can't just remove (self) here, because this is a touch function
170                 // called wheil C code is looping through area links...
171                 self.touch = SUB_Null;
172         }
173 };
174
175 void multi_use()
176 {
177         self.enemy = activator;
178         multi_trigger();
179 };
180
181 void multi_touch()
182 {
183         if not(self.spawnflags & 2)
184         {
185                 if not(other.iscreature)
186                         return;
187
188                 if(self.team)
189                 if(self.team == other.team)
190                         return;
191         }
192
193 // if the trigger has an angles field, check player's facing direction
194         if (self.movedir != '0 0 0')
195         {
196                 makevectors (other.angles);
197                 if (v_forward * self.movedir < 0)
198                         return;         // not facing the right way
199         }
200
201         EXACTTRIGGER_TOUCH;
202
203         self.enemy = other;
204         multi_trigger ();
205 };
206
207 void multi_eventdamage (vector hitloc, float damage, entity inflictor, entity attacker, float deathtype)
208 {
209         if (!self.takedamage)
210                 return;
211         self.health = self.health - damage;
212         if (self.health <= 0)
213         {
214                 self.enemy = attacker;
215                 multi_trigger();
216         }
217 }
218
219 void multi_reset()
220 {
221         self.touch = multi_touch;
222         self.health = self.max_health;
223         self.takedamage = DAMAGE_YES;
224         self.solid = SOLID_BBOX;
225         self.think = SUB_Null;
226         self.team = self.team_saved;
227 }
228
229 /*QUAKED spawnfunc_trigger_multiple (.5 .5 .5) ? notouch
230 Variable sized repeatable trigger.  Must be targeted at one or more entities.  If "health" is set, the trigger must be killed to activate each time.
231 If "delay" is set, the trigger waits some time after activating before firing.
232 "wait" : Seconds between triggerings. (.2 default)
233 If notouch is set, the trigger is only fired by other entities, not by touching.
234 NOTOUCH has been obsoleted by spawnfunc_trigger_relay!
235 sounds
236 1)      secret
237 2)      beep beep
238 3)      large switch
239 4)
240 set "message" to text string
241 */
242 void spawnfunc_trigger_multiple()
243 {
244         self.reset = multi_reset;
245         if (self.sounds == 1)
246         {
247                 precache_sound ("misc/secret.wav");
248                 self.noise = "misc/secret.wav";
249         }
250         else if (self.sounds == 2)
251         {
252                 precache_sound ("misc/talk.wav");
253                 self.noise = "misc/talk.wav";
254         }
255         else if (self.sounds == 3)
256         {
257                 precache_sound ("misc/trigger1.wav");
258                 self.noise = "misc/trigger1.wav";
259         }
260
261         if (!self.wait)
262                 self.wait = 0.2;
263         self.use = multi_use;
264
265         EXACTTRIGGER_INIT;
266
267         self.team_saved = self.team;
268
269         if (self.health)
270         {
271                 if (self.spawnflags & SPAWNFLAG_NOTOUCH)
272                         objerror ("health and notouch don't make sense\n");
273                 self.max_health = self.health;
274                 self.event_damage = multi_eventdamage;
275                 self.takedamage = DAMAGE_YES;
276                 self.solid = SOLID_BBOX;
277                 setorigin (self, self.origin);  // make sure it links into the world
278         }
279         else
280         {
281                 if ( !(self.spawnflags & SPAWNFLAG_NOTOUCH) )
282                 {
283                         self.touch = multi_touch;
284                         setorigin (self, self.origin);  // make sure it links into the world
285                 }
286         }
287 };
288
289
290 /*QUAKED spawnfunc_trigger_once (.5 .5 .5) ? notouch
291 Variable sized trigger. Triggers once, then removes itself.  You must set the key "target" to the name of another object in the level that has a matching
292 "targetname".  If "health" is set, the trigger must be killed to activate.
293 If notouch is set, the trigger is only fired by other entities, not by touching.
294 if "killtarget" is set, any objects that have a matching "target" will be removed when the trigger is fired.
295 if "angle" is set, the trigger will only fire when someone is facing the direction of the angle.  Use "360" for an angle of 0.
296 sounds
297 1)      secret
298 2)      beep beep
299 3)      large switch
300 4)
301 set "message" to text string
302 */
303 void spawnfunc_trigger_once()
304 {
305         self.wait = -1;
306         spawnfunc_trigger_multiple();
307 };
308
309 //=============================================================================
310
311 /*QUAKED spawnfunc_trigger_relay (.5 .5 .5) (-8 -8 -8) (8 8 8)
312 This fixed size trigger cannot be touched, it can only be fired by other events.  It can contain killtargets, targets, delays, and messages.
313 */
314 void spawnfunc_trigger_relay()
315 {
316         self.use = SUB_UseTargets;
317         self.reset = spawnfunc_trigger_relay; // this spawnfunc resets fully
318 };
319
320 void delay_use()
321 {
322     self.think = SUB_UseTargets;
323     self.nextthink = self.wait;
324 }
325
326 void delay_reset()
327 {
328         self.think = SUB_Null;
329 }
330
331 void spawnfunc_trigger_delay()
332 {
333     if(!self.wait)
334         self.wait = 1;
335
336     self.use = delay_use;
337     self.reset = delay_reset;
338 }
339
340 //=============================================================================
341
342
343 void counter_use()
344 {
345         self.count = self.count - 1;
346         if (self.count < 0)
347                 return;
348
349         if (self.count != 0)
350         {
351                 if (activator.classname == "player"
352                 && (self.spawnflags & SPAWNFLAG_NOMESSAGE) == 0)
353                 {
354                         if (self.count >= 4)
355                                 centerprint (activator, "There are more to go...");
356                         else if (self.count == 3)
357                                 centerprint (activator, "Only 3 more to go...");
358                         else if (self.count == 2)
359                                 centerprint (activator, "Only 2 more to go...");
360                         else
361                                 centerprint (activator, "Only 1 more to go...");
362                 }
363                 return;
364         }
365
366         if (activator.classname == "player"
367         && (self.spawnflags & SPAWNFLAG_NOMESSAGE) == 0)
368                 centerprint(activator, "Sequence completed!");
369         self.enemy = activator;
370         multi_trigger ();
371 };
372
373 void counter_reset()
374 {
375         self.count = self.cnt;
376         multi_reset();
377 }
378
379 /*QUAKED spawnfunc_trigger_counter (.5 .5 .5) ? nomessage
380 Acts as an intermediary for an action that takes multiple inputs.
381
382 If nomessage is not set, t will print "1 more.. " etc when triggered and "sequence complete" when finished.
383
384 After the counter has been triggered "count" times (default 2), it will fire all of it's targets and remove itself.
385 */
386 void spawnfunc_trigger_counter()
387 {
388         self.wait = -1;
389         if (!self.count)
390                 self.count = 2;
391         self.cnt = self.count;
392
393         self.use = counter_use;
394         self.reset = counter_reset;
395 };
396
397 .float triggerhurttime;
398 void trigger_hurt_touch()
399 {
400         // only do the EXACTTRIGGER_TOUCH checks when really needed (saves some cpu)
401         if (other.iscreature)
402         {
403                 if (other.takedamage)
404                 if (other.triggerhurttime < time)
405                 {
406                         EXACTTRIGGER_TOUCH;
407                         other.triggerhurttime = time + 1;
408                         Damage (other, self, self, self.dmg, DEATH_HURTTRIGGER, other.origin, '0 0 0');
409                 }
410         }
411         else
412         {
413                 if (!other.owner)
414                 {
415                         if (other.items & IT_KEY1 || other.items & IT_KEY2)     // reset flag
416                         {
417                                 EXACTTRIGGER_TOUCH;
418                                 other.pain_finished = min(other.pain_finished, time + 2);
419                         }
420                         else if (other.classname == "rune")                     // reset runes
421                         {
422                                 EXACTTRIGGER_TOUCH;
423                                 other.nextthink = min(other.nextthink, time + 1);
424                         }
425                 }
426         }
427
428         return;
429 };
430
431 /*QUAKED spawnfunc_trigger_hurt (.5 .5 .5) ?
432 Any object touching this will be hurt
433 set dmg to damage amount
434 defalt dmg = 5
435 */
436 .entity trigger_hurt_next;
437 entity trigger_hurt_last;
438 entity trigger_hurt_first;
439 void spawnfunc_trigger_hurt()
440 {
441         EXACTTRIGGER_INIT;
442         self.touch = trigger_hurt_touch;
443         if (!self.dmg)
444                 self.dmg = 1000;
445         if (!self.message)
446                 self.message = "was in the wrong place";
447         if (!self.message2)
448                 self.message2 = "was thrown into a world of hurt by";
449
450         if(!trigger_hurt_first)
451                 trigger_hurt_first = self;
452         if(trigger_hurt_last)
453                 trigger_hurt_last.trigger_hurt_next = self;
454         trigger_hurt_last = self;
455 };
456
457 float tracebox_hits_trigger_hurt(vector start, vector mi, vector ma, vector end)
458 {
459         entity th;
460
461         for(th = trigger_hurt_first; th; th = th.trigger_hurt_next)
462                 if(tracebox_hits_box(start, mi, ma, end, th.absmin, th.absmax))
463                         return TRUE;
464
465         return FALSE;
466 }
467
468 //////////////////////////////////////////////////////////////
469 //
470 //
471 //
472 //Trigger heal --a04191b92fbd93aa67214ef7e72d6d2e
473 //
474 //////////////////////////////////////////////////////////////
475
476 .float triggerhealtime;
477 void trigger_heal_touch()
478 {
479         // only do the EXACTTRIGGER_TOUCH checks when really needed (saves some cpu)
480         if (other.iscreature)
481         {
482                 if (other.takedamage)
483                 if (other.triggerhealtime < time)
484                 {
485                         EXACTTRIGGER_TOUCH;
486                         other.triggerhealtime = time + 1;
487                         
488                         if (other.health < self.max_health)
489                         {
490                                 other.health = min(other.health + self.health, self.max_health);
491                                 other.pauserothealth_finished = max(other.pauserothealth_finished, time + cvar("g_balance_pause_health_rot"));
492                                 sound (other, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NORM);
493                         }
494                 }
495         }
496 };
497
498 void spawnfunc_trigger_heal()
499 {
500         EXACTTRIGGER_INIT;
501         self.touch = trigger_heal_touch;
502         if (!self.health)
503                 self.health = 10;
504         if (!self.max_health)
505                 self.max_health = 200; //Max health topoff for field
506         if(self.noise == "")
507                 self.noise = "misc/mediumhealth.wav";
508         precache_sound(self.noise);
509 };
510
511
512 //////////////////////////////////////////////////////////////
513 //
514 //
515 //
516 //End trigger_heal
517 //
518 //////////////////////////////////////////////////////////////
519
520
521
522 // TODO add a way to do looped sounds with sound(); then complete this entity
523 .float volume, atten;
524 void target_speaker_use() {sound(self, CHAN_TRIGGER, self.noise, VOL_BASE * self.volume, self.atten);}
525
526 void spawnfunc_target_speaker()
527 {
528         if(self.noise)
529                 precache_sound (self.noise);
530         IFTARGETED
531         {
532                 if(!self.atten)
533                         self.atten = ATTN_NORM;
534                 else if(self.atten < 0)
535                         self.atten = 0;
536                 if(!self.volume)
537                         self.volume = 1;
538                 self.use = target_speaker_use;
539         }
540         else
541         {
542                 if(!self.atten)
543                         self.atten = ATTN_STATIC;
544                 else if(self.atten < 0)
545                         self.atten = 0;
546                 if(!self.volume)
547                         self.volume = 1;
548                 ambientsound (self.origin, self.noise, VOL_BASE * self.volume, self.atten);
549         }
550 };
551
552
553 void spawnfunc_func_stardust() {
554         self.effects = EF_STARDUST;
555 }
556
557 .string bgmscript;
558 .float bgmscriptattack;
559 .float bgmscriptdecay;
560 .float bgmscriptsustain;
561 .float bgmscriptrelease;
562 float pointparticles_SendEntity(entity to, float fl)
563 {
564         WriteByte(MSG_ENTITY, ENT_CLIENT_POINTPARTICLES);
565
566         // optional features to save space
567         fl = fl & 0x0F;
568         if(self.spawnflags & 2)
569                 fl |= 0x10; // absolute count on toggle-on
570         if(self.movedir != '0 0 0' || self.velocity != '0 0 0')
571                 fl |= 0x20; // 4 bytes - saves CPU
572         if(self.waterlevel || self.count != 1)
573                 fl |= 0x40; // 4 bytes - obscure features almost never used
574         if(self.mins != '0 0 0' || self.maxs != '0 0 0')
575                 fl |= 0x80; // 14 bytes - saves lots of space
576
577         WriteByte(MSG_ENTITY, fl);
578         if(fl & 2)
579         {
580                 if(self.state)
581                         WriteCoord(MSG_ENTITY, self.impulse);
582                 else
583                         WriteCoord(MSG_ENTITY, 0); // off
584         }
585         if(fl & 4)
586         {
587                 WriteCoord(MSG_ENTITY, self.origin_x);
588                 WriteCoord(MSG_ENTITY, self.origin_y);
589                 WriteCoord(MSG_ENTITY, self.origin_z);
590         }
591         if(fl & 1)
592         {
593                 if(self.model != "null")
594                 {
595                         WriteShort(MSG_ENTITY, self.modelindex);
596                         if(fl & 0x80)
597                         {
598                                 WriteCoord(MSG_ENTITY, self.mins_x);
599                                 WriteCoord(MSG_ENTITY, self.mins_y);
600                                 WriteCoord(MSG_ENTITY, self.mins_z);
601                                 WriteCoord(MSG_ENTITY, self.maxs_x);
602                                 WriteCoord(MSG_ENTITY, self.maxs_y);
603                                 WriteCoord(MSG_ENTITY, self.maxs_z);
604                         }
605                 }
606                 else
607                 {
608                         WriteShort(MSG_ENTITY, 0);
609                         if(fl & 0x80)
610                         {
611                                 WriteCoord(MSG_ENTITY, self.maxs_x);
612                                 WriteCoord(MSG_ENTITY, self.maxs_y);
613                                 WriteCoord(MSG_ENTITY, self.maxs_z);
614                         }
615                 }
616                 WriteShort(MSG_ENTITY, self.cnt);
617                 if(fl & 0x20)
618                 {
619                         WriteShort(MSG_ENTITY, compressShortVector(self.velocity));
620                         WriteShort(MSG_ENTITY, compressShortVector(self.movedir));
621                 }
622                 if(fl & 0x40)
623                 {
624                         WriteShort(MSG_ENTITY, self.waterlevel * 16.0);
625                         WriteByte(MSG_ENTITY, self.count * 16.0);
626                 }
627                 WriteString(MSG_ENTITY, self.noise);
628                 if(self.noise != "")
629                 {
630                         WriteByte(MSG_ENTITY, floor(self.atten * 64));
631                         WriteByte(MSG_ENTITY, floor(self.volume * 255));
632                 }
633                 WriteString(MSG_ENTITY, self.bgmscript);
634                 if(self.bgmscript != "")
635                 {
636                         WriteByte(MSG_ENTITY, floor(self.bgmscriptattack * 64));
637                         WriteByte(MSG_ENTITY, floor(self.bgmscriptdecay * 64));
638                         WriteByte(MSG_ENTITY, floor(self.bgmscriptsustain * 255));
639                         WriteByte(MSG_ENTITY, floor(self.bgmscriptrelease * 64));
640                 }
641         }
642         return 1;
643 }
644
645 void pointparticles_use()
646 {
647         self.state = !self.state;
648         self.SendFlags |= 2;
649 }
650
651 void pointparticles_think()
652 {
653         if(self.origin != self.oldorigin)
654         {
655                 self.SendFlags |= 4;
656                 self.oldorigin = self.origin;
657         }
658         self.nextthink = time;
659 }
660
661 void pointparticles_reset()
662 {
663         if(self.spawnflags & 1)
664                 self.state = 1;
665         else
666                 self.state = 0;
667 }
668
669 void spawnfunc_func_pointparticles()
670 {
671         if(self.model != "")
672                 setmodel(self, self.model);
673         if(self.noise != "")
674                 precache_sound (self.noise);
675         
676         if(!self.bgmscriptsustain)
677                 self.bgmscriptsustain = 1;
678         else if(self.bgmscriptsustain < 0)
679                 self.bgmscriptsustain = 0;
680
681         if(!self.atten)
682                 self.atten = ATTN_NORM;
683         else if(self.atten < 0)
684                 self.atten = 0;
685         if(!self.volume)
686                 self.volume = 1;
687         if(!self.count)
688                 self.count = 1;
689         if(!self.impulse)
690                 self.impulse = 1;
691
692         if(!self.modelindex)
693         {
694                 setorigin(self, self.origin + self.mins);
695                 setsize(self, '0 0 0', self.maxs - self.mins);
696         }
697         if(!self.cnt)
698                 self.cnt = particleeffectnum(self.mdl);
699
700         Net_LinkEntity(self, FALSE, 0, pointparticles_SendEntity);
701
702         IFTARGETED
703         {
704                 self.use = pointparticles_use;
705                 self.reset = pointparticles_reset;
706                 self.reset();
707         }
708         else
709                 self.state = 1;
710         self.think = pointparticles_think;
711         self.nextthink = time;
712 }
713
714 void spawnfunc_func_sparks()
715 {
716         // self.cnt is the amount of sparks that one burst will spawn
717         if(self.cnt < 1) {
718                 self.cnt = 25.0; // nice default value
719         }
720
721         // self.wait is the probability that a sparkthink will spawn a spark shower
722         // range: 0 - 1, but 0 makes little sense, so...
723         if(self.wait < 0.05) {
724                 self.wait = 0.25; // nice default value
725         }
726
727         self.count = self.cnt;
728         self.mins = '0 0 0';
729         self.maxs = '0 0 0';
730         self.velocity = '0 0 -1';
731         self.mdl = "TE_SPARK";
732         self.impulse = 10 * self.wait; // by default 2.5/sec
733         self.wait = 0;
734         self.cnt = 0; // use mdl
735
736         spawnfunc_func_pointparticles();
737 }
738
739 float rainsnow_SendEntity(entity to, float sf)
740 {
741         WriteByte(MSG_ENTITY, ENT_CLIENT_RAINSNOW);
742         WriteByte(MSG_ENTITY, self.state);
743         WriteCoord(MSG_ENTITY, self.origin_x + self.mins_x);
744         WriteCoord(MSG_ENTITY, self.origin_y + self.mins_y);
745         WriteCoord(MSG_ENTITY, self.origin_z + self.mins_z);
746         WriteCoord(MSG_ENTITY, self.maxs_x - self.mins_x);
747         WriteCoord(MSG_ENTITY, self.maxs_y - self.mins_y);
748         WriteCoord(MSG_ENTITY, self.maxs_z - self.mins_z);
749         WriteShort(MSG_ENTITY, compressShortVector(self.dest));
750         WriteShort(MSG_ENTITY, self.count);
751         WriteByte(MSG_ENTITY, self.cnt);
752         return 1;
753 };
754
755 /*QUAKED spawnfunc_func_rain (0 .5 .8) ?
756 This is an invisible area like a trigger, which rain falls inside of.
757
758 Keys:
759 "velocity"
760  falling direction (should be something like '0 0 -700', use the X and Y velocity for wind)
761 "cnt"
762  sets color of rain (default 12 - white)
763 "count"
764  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
765 */
766 void spawnfunc_func_rain()
767 {
768         self.dest = self.velocity;
769         self.velocity = '0 0 0';
770         if (!self.dest)
771                 self.dest = '0 0 -700';
772         self.angles = '0 0 0';
773         self.movetype = MOVETYPE_NONE;
774         self.solid = SOLID_NOT;
775         SetBrushEntityModel();
776         if (!self.cnt)
777                 self.cnt = 12;
778         if (!self.count)
779                 self.count = 2000;
780         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
781         if (self.count < 1)
782                 self.count = 1;
783         if(self.count > 65535)
784                 self.count = 65535;
785
786         self.state = 1; // 1 is rain, 0 is snow
787         self.Version = 1;
788
789         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
790 };
791
792
793 /*QUAKED spawnfunc_func_snow (0 .5 .8) ?
794 This is an invisible area like a trigger, which snow falls inside of.
795
796 Keys:
797 "velocity"
798  falling direction (should be something like '0 0 -300', use the X and Y velocity for wind)
799 "cnt"
800  sets color of rain (default 12 - white)
801 "count"
802  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
803 */
804 void spawnfunc_func_snow()
805 {
806         self.dest = self.velocity;
807         self.velocity = '0 0 0';
808         if (!self.dest)
809                 self.dest = '0 0 -300';
810         self.angles = '0 0 0';
811         self.movetype = MOVETYPE_NONE;
812         self.solid = SOLID_NOT;
813         SetBrushEntityModel();
814         if (!self.cnt)
815                 self.cnt = 12;
816         if (!self.count)
817                 self.count = 2000;
818         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
819         if (self.count < 1)
820                 self.count = 1;
821         if(self.count > 65535)
822                 self.count = 65535;
823
824         self.state = 0; // 1 is rain, 0 is snow
825         self.Version = 1;
826
827         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
828 };
829
830
831 void FireRailgunBullet (vector start, vector end, float bdamage, float bforce, float deathtype);
832
833 .float modelscale;
834 void misc_laser_aim()
835 {
836         vector a;
837         if(self.enemy)
838         {
839                 if(self.spawnflags & 2)
840                 {
841                         if(self.enemy.origin != self.mangle)
842                         {
843                                 self.mangle = self.enemy.origin;
844                                 self.SendFlags |= 2;
845                         }
846                 }
847                 else
848                 {
849                         a = vectoangles(self.enemy.origin - self.origin);
850                         a_x = -a_x;
851                         if(a != self.mangle)
852                         {
853                                 self.mangle = a;
854                                 self.SendFlags |= 2;
855                         }
856                 }
857         }
858         else
859         {
860                 if(self.angles != self.mangle)
861                 {
862                         self.mangle = self.angles;
863                         self.SendFlags |= 2;
864                 }
865         }
866         if(self.origin != self.oldorigin)
867         {
868                 self.SendFlags |= 1;
869                 self.oldorigin = self.origin;
870         }
871 }
872
873 void misc_laser_init()
874 {
875         if(self.target != "")
876                 self.enemy = find(world, targetname, self.target);
877 }
878
879 .entity pusher;
880 void misc_laser_think()
881 {
882         vector o;
883         entity oldself;
884
885         self.nextthink = time;
886
887         if(!self.state)
888                 return;
889
890         misc_laser_aim();
891
892         if(self.enemy)
893         {
894                 o = self.enemy.origin;
895                 if not(self.spawnflags & 2)
896                         o = self.origin + normalize(o - self.origin) * 32768;
897         }
898         else
899         {
900                 makevectors(self.mangle);
901                 o = self.origin + v_forward * 32768;
902         }
903
904         if(self.dmg)
905         {
906                 if(self.dmg < 0)
907                         FireRailgunBullet(self.origin, o, 100000, 0, DEATH_HURTTRIGGER);
908                 else
909                         FireRailgunBullet(self.origin, o, self.dmg * frametime, 0, DEATH_HURTTRIGGER);
910         }
911
912         if(self.enemy.target != "") // DETECTOR laser
913         {
914                 traceline(self.origin, o, MOVE_NORMAL, self);
915                 if(trace_ent.iscreature)
916                 {
917                         self.pusher = trace_ent;
918                         if(!self.count)
919                         {
920                                 self.count = 1;
921
922                                 oldself = self;
923                                 self = self.enemy;
924                                 activator = self.pusher;
925                                 SUB_UseTargets();
926                                 self = oldself;
927                         }
928                 }
929                 else
930                 {
931                         if(self.count)
932                         {
933                                 self.count = 0;
934
935                                 oldself = self;
936                                 self = self.enemy;
937                                 activator = self.pusher;
938                                 SUB_UseTargets();
939                                 self = oldself;
940                         }
941                 }
942         }
943 }
944
945 float laser_SendEntity(entity to, float fl)
946 {
947         WriteByte(MSG_ENTITY, ENT_CLIENT_LASER);
948         fl = fl - (fl & 0xE0); // use that bit to indicate finite length laser
949         if(self.spawnflags & 2)
950                 fl |= 0x80;
951         if(self.alpha)
952                 fl |= 0x40;
953         if(self.scale != 1 || self.modelscale != 1)
954                 fl |= 0x20;
955         WriteByte(MSG_ENTITY, fl);
956         if(fl & 1)
957         {
958                 WriteCoord(MSG_ENTITY, self.origin_x);
959                 WriteCoord(MSG_ENTITY, self.origin_y);
960                 WriteCoord(MSG_ENTITY, self.origin_z);
961         }
962         if(fl & 8)
963         {
964                 WriteByte(MSG_ENTITY, self.colormod_x * 255.0);
965                 WriteByte(MSG_ENTITY, self.colormod_y * 255.0);
966                 WriteByte(MSG_ENTITY, self.colormod_z * 255.0);
967                 if(fl & 0x40)
968                         WriteByte(MSG_ENTITY, self.alpha * 255.0);
969                 if(fl & 0x20)
970                 {
971                         WriteByte(MSG_ENTITY, bound(0, self.scale * 16.0, 255));
972                         WriteByte(MSG_ENTITY, bound(0, self.modelscale * 16.0, 255));
973                 }
974                 WriteShort(MSG_ENTITY, self.cnt + 1);
975         }
976         if(fl & 2)
977         {
978                 if(fl & 0x80)
979                 {
980                         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
981                         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
982                         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
983                 }
984                 else
985                 {
986                         WriteAngle(MSG_ENTITY, self.mangle_x);
987                         WriteAngle(MSG_ENTITY, self.mangle_y);
988                 }
989         }
990         if(fl & 4)
991                 WriteByte(MSG_ENTITY, self.state);
992         return 1;
993 }
994
995 /*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
996 Any object touching the beam will be hurt
997 Keys:
998 "target"
999  spawnfunc_target_position where the laser ends
1000 "mdl"
1001  name of beam end effect to use
1002 "colormod"
1003  color of the beam (default: red)
1004 "dmg"
1005  damage per second (-1 for a laser that kills immediately)
1006 */
1007 void laser_use()
1008 {
1009         self.state = !self.state;
1010         self.SendFlags |= 4;
1011         misc_laser_aim();
1012 }
1013
1014 void laser_reset()
1015 {
1016         if(self.spawnflags & 1)
1017                 self.state = 1;
1018         else
1019                 self.state = 0;
1020 }
1021
1022 void spawnfunc_misc_laser()
1023 {
1024         if(self.mdl)
1025         {
1026                 if(self.mdl == "none")
1027                         self.cnt = -1;
1028                 else
1029                 {
1030                         self.cnt = particleeffectnum(self.mdl);
1031                         if(self.cnt < 0)
1032                                 if(self.dmg)
1033                                         self.cnt = particleeffectnum("laser_deadly");
1034                 }
1035         }
1036         else if(!self.cnt)
1037         {
1038                 if(self.dmg)
1039                         self.cnt = particleeffectnum("laser_deadly");
1040                 else
1041                         self.cnt = -1;
1042         }
1043         if(self.cnt < 0)
1044                 self.cnt = -1;
1045
1046         if(self.colormod == '0 0 0')
1047                 if(!self.alpha)
1048                         self.colormod = '1 0 0';
1049         if(!self.message)
1050                 self.message = "saw the light";
1051         if (!self.message2)
1052                 self.message2 = "was pushed into a laser by";
1053         if(!self.scale)
1054                 self.scale = 1;
1055         if(!self.modelscale)
1056                 self.modelscale = 1;
1057         self.think = misc_laser_think;
1058         self.nextthink = time;
1059         InitializeEntity(self, misc_laser_init, INITPRIO_FINDTARGET);
1060
1061         self.mangle = self.angles;
1062
1063         Net_LinkEntity(self, FALSE, 0, laser_SendEntity);
1064
1065         IFTARGETED
1066         {
1067                 self.reset = laser_reset;
1068                 laser_reset();
1069                 self.use = laser_use;
1070         }
1071         else
1072                 self.state = 1;
1073 }
1074
1075 // tZorks trigger impulse / gravity
1076 .float radius;
1077 .float falloff;
1078 .float strength;
1079 .float lastpushtime;
1080
1081 // targeted (directional) mode
1082 void trigger_impulse_touch1()
1083 {
1084         entity targ;
1085     float pushdeltatime;
1086     float str;
1087
1088         // FIXME: Better checking for what to push and not.
1089         if not(other.iscreature)
1090         if (other.classname != "corpse")
1091         if (other.classname != "body")
1092         if (other.classname != "gib")
1093         if (other.classname != "missile")
1094         if (other.classname != "rocket")
1095         if (other.classname != "casing")
1096         if (other.classname != "grenade")
1097         if (other.classname != "plasma")
1098         if (other.classname != "plasma_prim")
1099         if (other.classname != "plasma_chain")
1100         if (other.classname != "droppedweapon")
1101                 return;
1102
1103         if (other.deadflag && other.iscreature)
1104                 return;
1105
1106         EXACTTRIGGER_TOUCH;
1107
1108     targ = find(world, targetname, self.target);
1109     if(!targ)
1110     {
1111         objerror("trigger_force without a (valid) .target!\n");
1112         remove(self);
1113         return;
1114     }
1115
1116     if(self.falloff == 1)
1117         str = (str / self.radius) * self.strength;
1118     else if(self.falloff == 2)
1119         str = (1 - (str / self.radius)) * self.strength;
1120     else
1121         str = self.strength;
1122
1123     pushdeltatime = time - other.lastpushtime;
1124     if (pushdeltatime > 0.15) pushdeltatime = 0;
1125     other.lastpushtime = time;
1126     if(!pushdeltatime) return;
1127
1128     other.velocity = other.velocity + normalize(targ.origin - self.origin) * self.strength * pushdeltatime;
1129         other.flags &~= FL_ONGROUND;
1130 }
1131
1132 // Directionless (accelerator/decelerator) mode
1133 void trigger_impulse_touch2()
1134 {
1135     float pushdeltatime;
1136
1137         // FIXME: Better checking for what to push and not.
1138         if not(other.iscreature)
1139         if (other.classname != "corpse")
1140         if (other.classname != "body")
1141         if (other.classname != "gib")
1142         if (other.classname != "missile")
1143         if (other.classname != "rocket")
1144         if (other.classname != "casing")
1145         if (other.classname != "grenade")
1146         if (other.classname != "plasma")
1147         if (other.classname != "plasma_prim")
1148         if (other.classname != "plasma_chain")
1149         if (other.classname != "droppedweapon")
1150                 return;
1151
1152         if (other.deadflag && other.iscreature)
1153                 return;
1154
1155         EXACTTRIGGER_TOUCH;
1156
1157     pushdeltatime = time - other.lastpushtime;
1158     if (pushdeltatime > 0.15) pushdeltatime = 0;
1159     other.lastpushtime = time;
1160     if(!pushdeltatime) return;
1161
1162     //if(self.strength > 1)
1163         other.velocity = other.velocity * (self.strength * pushdeltatime);
1164     //else
1165     //    other.velocity = other.velocity - (other.velocity * self.strength * pushdeltatime);
1166 }
1167
1168 // Spherical (gravity/repulsor) mode
1169 void trigger_impulse_touch3()
1170 {
1171     float pushdeltatime;
1172     float str;
1173
1174         // FIXME: Better checking for what to push and not.
1175         if not(other.iscreature)
1176         if (other.classname != "corpse")
1177         if (other.classname != "body")
1178         if (other.classname != "gib")
1179         if (other.classname != "missile")
1180         if (other.classname != "rocket")
1181         if (other.classname != "casing")
1182         if (other.classname != "grenade")
1183         if (other.classname != "plasma")
1184         if (other.classname != "plasma_prim")
1185         if (other.classname != "plasma_chain")
1186         if (other.classname != "droppedweapon")
1187                 return;
1188
1189         if (other.deadflag && other.iscreature)
1190                 return;
1191
1192         EXACTTRIGGER_TOUCH;
1193
1194     pushdeltatime = time - other.lastpushtime;
1195     if (pushdeltatime > 0.15) pushdeltatime = 0;
1196     other.lastpushtime = time;
1197     if(!pushdeltatime) return;
1198
1199     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1200
1201         str = min(self.radius, vlen(self.origin - other.origin));
1202
1203     if(self.falloff == 1)
1204         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1205     else if(self.falloff == 2)
1206         str = (str / self.radius) * self.strength; // 0 in the inside
1207     else
1208         str = self.strength;
1209
1210     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1211 }
1212
1213 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1214 -------- KEYS --------
1215 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1216          If not, this trigger acts like a damper/accelerator field.
1217
1218 strength : This is how mutch force to add in the direction of .target each second
1219            when .target is set. If not, this is hoe mutch to slow down/accelerate
1220            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1221
1222 radius   : If set, act as a spherical device rather then a liniar one.
1223
1224 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1225
1226 -------- NOTES --------
1227 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1228 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1229 */
1230
1231 void spawnfunc_trigger_impulse()
1232 {
1233         EXACTTRIGGER_INIT;
1234     if(self.radius)
1235     {
1236         if(!self.strength) self.strength = 2000;
1237         setorigin(self, self.origin);
1238         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1239         self.touch = trigger_impulse_touch3;
1240     }
1241     else
1242     {
1243         if(self.target)
1244         {
1245             if(!self.strength) self.strength = 950;
1246             self.touch = trigger_impulse_touch1;
1247         }
1248         else
1249         {
1250             if(!self.strength) self.strength = 0.9;
1251             self.touch = trigger_impulse_touch2;
1252         }
1253     }
1254 }
1255
1256 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1257 "Flip-flop" trigger gate... lets only every second trigger event through
1258 */
1259 void flipflop_use()
1260 {
1261         self.state = !self.state;
1262         if(self.state)
1263                 SUB_UseTargets();
1264 }
1265
1266 void spawnfunc_trigger_flipflop()
1267 {
1268         if(self.spawnflags & 1)
1269                 self.state = 1;
1270         self.use = flipflop_use;
1271         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1272 }
1273
1274 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1275 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1276 */
1277 void monoflop_use()
1278 {
1279         self.nextthink = time + self.wait;
1280         self.enemy = activator;
1281         if(self.state)
1282                 return;
1283         self.state = 1;
1284         SUB_UseTargets();
1285 }
1286 void monoflop_fixed_use()
1287 {
1288         if(self.state)
1289                 return;
1290         self.nextthink = time + self.wait;
1291         self.state = 1;
1292         self.enemy = activator;
1293         SUB_UseTargets();
1294 }
1295
1296 void monoflop_think()
1297 {
1298         self.state = 0;
1299         activator = self.enemy;
1300         SUB_UseTargets();
1301 }
1302
1303 void monoflop_reset()
1304 {
1305         self.state = 0;
1306         self.nextthink = 0;
1307 }
1308
1309 void spawnfunc_trigger_monoflop()
1310 {
1311         if(!self.wait)
1312                 self.wait = 1;
1313         if(self.spawnflags & 1)
1314                 self.use = monoflop_fixed_use;
1315         else
1316                 self.use = monoflop_use;
1317         self.think = monoflop_think;
1318         self.state = 0;
1319         self.reset = monoflop_reset;
1320 }
1321
1322 void multivibrator_send()
1323 {
1324         float newstate;
1325         float cyclestart;
1326
1327         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1328
1329         newstate = (time < cyclestart + self.wait);
1330
1331         activator = self;
1332         if(self.state != newstate)
1333                 SUB_UseTargets();
1334         self.state = newstate;
1335
1336         if(self.state)
1337                 self.nextthink = cyclestart + self.wait + 0.01;
1338         else
1339                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1340 }
1341
1342 void multivibrator_toggle()
1343 {
1344         if(self.nextthink == 0)
1345         {
1346                 multivibrator_send();
1347         }
1348         else
1349         {
1350                 if(self.state)
1351                 {
1352                         SUB_UseTargets();
1353                         self.state = 0;
1354                 }
1355                 self.nextthink = 0;
1356         }
1357 }
1358
1359 void multivibrator_reset()
1360 {
1361         if(!(self.spawnflags & 1))
1362                 self.nextthink = 0; // wait for a trigger event
1363         else
1364                 self.nextthink = max(1, time);
1365 }
1366
1367 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1368 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1369 -------- KEYS --------
1370 target: trigger all entities with this targetname when it goes off
1371 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1372 phase: offset of the timing
1373 wait: "on" cycle time (default: 1)
1374 respawntime: "off" cycle time (default: same as wait)
1375 -------- SPAWNFLAGS --------
1376 START_ON: assume it is already turned on (when targeted)
1377 */
1378 void spawnfunc_trigger_multivibrator()
1379 {
1380         if(!self.wait)
1381                 self.wait = 1;
1382         if(!self.respawntime)
1383                 self.respawntime = self.wait;
1384
1385         self.state = 0;
1386         self.use = multivibrator_toggle;
1387         self.think = multivibrator_send;
1388         self.nextthink = time;
1389
1390         IFTARGETED
1391                 multivibrator_reset();
1392 }
1393
1394
1395 void follow_init()
1396 {
1397         entity src, dst;
1398         src = find(world, targetname, self.killtarget);
1399         dst = find(world, targetname, self.target);
1400
1401         if(!src || !dst)
1402         {
1403                 objerror("follow: could not find target/killtarget");
1404                 return;
1405         }
1406
1407         if(self.spawnflags & 1)
1408         {
1409                 // attach
1410                 if(self.spawnflags & 2)
1411                 {
1412                         setattachment(dst, src, self.message);
1413                 }
1414                 else
1415                 {
1416                         attach_sameorigin(dst, src, self.message);
1417                 }
1418         }
1419         else
1420         {
1421                 if(self.spawnflags & 2)
1422                 {
1423                         dst.movetype = MOVETYPE_FOLLOW;
1424                         dst.aiment = src;
1425                         // dst.punchangle = '0 0 0'; // keep unchanged
1426                         dst.view_ofs = dst.origin;
1427                         dst.v_angle = dst.angles;
1428                 }
1429                 else
1430                 {
1431                         follow_sameorigin(dst, src);
1432                 }
1433         }
1434
1435         remove(self);
1436 }
1437
1438 void spawnfunc_misc_follow()
1439 {
1440         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1441 }
1442
1443
1444
1445 void gamestart_use() {
1446         activator = self;
1447         SUB_UseTargets();
1448         remove(self);
1449 }
1450
1451 void spawnfunc_trigger_gamestart() {
1452         self.use = gamestart_use;
1453         self.reset2 = spawnfunc_trigger_gamestart;
1454
1455         if(self.wait)
1456         {
1457                 self.think = self.use;
1458                 self.nextthink = game_starttime + self.wait;
1459         }
1460         else
1461                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1462 }
1463
1464
1465
1466
1467 .entity voicescript; // attached voice script
1468 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1469 .float voicescript_nextthink; // time to play next voice
1470 .float voicescript_voiceend; // time when this voice ends
1471
1472 void target_voicescript_clear(entity pl)
1473 {
1474         pl.voicescript = world;
1475 }
1476
1477 void target_voicescript_use()
1478 {
1479         if(activator.voicescript != self)
1480         {
1481                 activator.voicescript = self;
1482                 activator.voicescript_index = 0;
1483                 activator.voicescript_nextthink = time + self.delay;
1484         }
1485 }
1486
1487 void target_voicescript_next(entity pl)
1488 {
1489         entity vs;
1490         float i, n;
1491
1492         vs = pl.voicescript;
1493         if(!vs)
1494                 return;
1495         if(vs.message == "")
1496                 return;
1497         if(pl.classname != "player")
1498                 return;
1499         if(gameover)
1500                 return;
1501
1502         if(time >= pl.voicescript_voiceend)
1503         {
1504                 if(time >= pl.voicescript_nextthink)
1505                 {
1506                         // get the next voice...
1507                         n = tokenize_sane(vs.message);
1508
1509                         if(pl.voicescript_index < vs.cnt)
1510                                 i = pl.voicescript_index * 2;
1511                         else if(n > vs.cnt * 2)
1512                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1513                         else
1514                                 i = -1;
1515
1516                         if(i >= 0)
1517                         {
1518                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1519                                 pl.voicescript_voiceend = time + stof(argv(i + 1));
1520                         }
1521                         else
1522                                 pl.voicescript = world;
1523
1524                         pl.voicescript_index += 1;
1525                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1526                 }
1527         }
1528 }
1529
1530 void spawnfunc_target_voicescript()
1531 {
1532         // netname: directory of the sound files
1533         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1534         //          foo1 4.1 foo2 4.0 foo3 3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1535         // wait: average time between messages
1536         // delay: initial delay before the first message
1537         
1538         float i, n;
1539         self.use = target_voicescript_use;
1540
1541         n = tokenize_sane(self.message);
1542         self.cnt = n / 2;
1543         for(i = 0; i+1 < n; i += 2)
1544         {
1545                 if(argv(i) == "*")
1546                 {
1547                         self.cnt = i / 2;
1548                         ++i;
1549                 }
1550                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1551         }
1552 }
1553
1554
1555
1556 void trigger_relay_teamcheck_use()
1557 {
1558         if(activator.team)
1559         {
1560                 if(self.spawnflags & 2)
1561                 {
1562                         if(activator.team != self.team)
1563                                 SUB_UseTargets();
1564                 }
1565                 else
1566                 {
1567                         if(activator.team == self.team)
1568                                 SUB_UseTargets();
1569                 }
1570         }
1571         else
1572         {
1573                 if(self.spawnflags & 1)
1574                         SUB_UseTargets();
1575         }
1576 }
1577
1578 void trigger_relay_teamcheck_reset()
1579 {
1580         self.team = self.team_saved;
1581 }
1582
1583 void spawnfunc_trigger_relay_teamcheck()
1584 {
1585         self.team_saved = self.team;
1586         self.use = trigger_relay_teamcheck_use;
1587         self.reset = trigger_relay_teamcheck_reset;
1588 }
1589
1590
1591
1592 void trigger_disablerelay_use()
1593 {
1594         entity e;
1595
1596         float a, b;
1597         a = b = 0;
1598
1599         for(e = world; (e = find(e, targetname, self.target)); )
1600         {
1601                 if(e.use == SUB_UseTargets)
1602                 {
1603                         e.use = SUB_DontUseTargets;
1604                         ++a;
1605                 }
1606                 else if(e.use == SUB_DontUseTargets)
1607                 {
1608                         e.use = SUB_UseTargets;
1609                         ++b;
1610                 }
1611         }
1612
1613         if(!a == !b)
1614                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1615 }
1616
1617 void spawnfunc_trigger_disablerelay()
1618 {
1619         self.use = trigger_disablerelay_use;
1620 }