]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/g_triggers.qc
remove pointparticles glow_color (does not work anyway)
[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 bgmscriptdecay;
559 float pointparticles_SendEntity(entity to, float fl)
560 {
561         WriteByte(MSG_ENTITY, ENT_CLIENT_POINTPARTICLES);
562
563         // optional features to save space
564         fl = fl & 0x0F;
565         if(self.spawnflags & 1)
566                 fl |= 0x10; // absolute count on toggle-on
567         if(self.movedir != '0 0 0' || self.velocity != '0 0 0')
568                 fl |= 0x20; // 4 bytes - saves CPU
569         if(self.waterlevel || self.count != 1)
570                 fl |= 0x40; // 4 bytes - obscure features almost never used
571         if(self.mins != '0 0 0' || self.maxs != '0 0 0')
572                 fl |= 0x80; // 14 bytes - saves lots of space
573
574         WriteByte(MSG_ENTITY, fl);
575         if(fl & 2)
576         {
577                 if(self.state)
578                         WriteCoord(MSG_ENTITY, self.impulse);
579                 else
580                         WriteCoord(MSG_ENTITY, 0); // off
581         }
582         if(fl & 4)
583         {
584                 WriteCoord(MSG_ENTITY, self.origin_x);
585                 WriteCoord(MSG_ENTITY, self.origin_y);
586                 WriteCoord(MSG_ENTITY, self.origin_z);
587         }
588         if(fl & 1)
589         {
590                 if(self.model != "null")
591                 {
592                         WriteShort(MSG_ENTITY, self.modelindex);
593                         if(fl & 0x80)
594                         {
595                                 WriteCoord(MSG_ENTITY, self.mins_x);
596                                 WriteCoord(MSG_ENTITY, self.mins_y);
597                                 WriteCoord(MSG_ENTITY, self.mins_z);
598                                 WriteCoord(MSG_ENTITY, self.maxs_x);
599                                 WriteCoord(MSG_ENTITY, self.maxs_y);
600                                 WriteCoord(MSG_ENTITY, self.maxs_z);
601                         }
602                 }
603                 else
604                 {
605                         WriteShort(MSG_ENTITY, 0);
606                         if(fl & 0x80)
607                         {
608                                 WriteCoord(MSG_ENTITY, self.maxs_x);
609                                 WriteCoord(MSG_ENTITY, self.maxs_y);
610                                 WriteCoord(MSG_ENTITY, self.maxs_z);
611                         }
612                 }
613                 WriteShort(MSG_ENTITY, self.cnt);
614                 if(fl & 0x20)
615                 {
616                         WriteShort(MSG_ENTITY, compressShortVector(self.velocity));
617                         WriteShort(MSG_ENTITY, compressShortVector(self.movedir));
618                 }
619                 if(fl & 0x40)
620                 {
621                         WriteShort(MSG_ENTITY, self.waterlevel * 16.0);
622                         WriteByte(MSG_ENTITY, self.count * 16.0);
623                 }
624                 WriteString(MSG_ENTITY, self.noise);
625                 if(self.noise != "")
626                 {
627                         WriteByte(MSG_ENTITY, floor(self.atten * 64));
628                         WriteByte(MSG_ENTITY, floor(self.volume * 255));
629                 }
630                 WriteString(MSG_ENTITY, self.bgmscript);
631                 if(self.bgmscript != "")
632                         WriteByte(MSG_ENTITY, floor(self.bgmscriptdecay * 255));
633         }
634         return 1;
635 }
636
637 void pointparticles_use()
638 {
639         self.state = !self.state;
640         self.SendFlags |= 2;
641 }
642
643 void pointparticles_think()
644 {
645         if(self.origin != self.oldorigin)
646         {
647                 self.SendFlags |= 4;
648                 self.oldorigin = self.origin;
649         }
650         self.nextthink = time;
651 }
652
653 void pointparticles_reset()
654 {
655         if(self.spawnflags & 1)
656                 self.state = 1;
657         else
658                 self.state = 0;
659 }
660
661 void spawnfunc_func_pointparticles()
662 {
663         if(self.model != "")
664                 setmodel(self, self.model);
665         if(self.noise != "")
666                 precache_sound (self.noise);
667
668         if(!self.atten)
669                 self.atten = ATTN_NORM;
670         else if(self.atten < 0)
671                 self.atten = 0;
672         if(!self.volume)
673                 self.volume = 1;
674         if(!self.count)
675                 self.count = 1;
676         if(!self.impulse)
677                 self.impulse = 1;
678
679         if(!self.modelindex)
680         {
681                 setorigin(self, self.origin + self.mins);
682                 setsize(self, '0 0 0', self.maxs - self.mins);
683         }
684         if(!self.cnt)
685                 self.cnt = particleeffectnum(self.mdl);
686
687         Net_LinkEntity(self, FALSE, 0, pointparticles_SendEntity);
688
689         IFTARGETED
690         {
691                 self.use = pointparticles_use;
692                 self.reset = pointparticles_reset;
693                 self.reset();
694         }
695         else
696                 self.state = 1;
697         self.think = pointparticles_think;
698         self.nextthink = time;
699 }
700
701 void spawnfunc_func_sparks()
702 {
703         // self.cnt is the amount of sparks that one burst will spawn
704         if(self.cnt < 1) {
705                 self.cnt = 25.0; // nice default value
706         }
707
708         // self.wait is the probability that a sparkthink will spawn a spark shower
709         // range: 0 - 1, but 0 makes little sense, so...
710         if(self.wait < 0.05) {
711                 self.wait = 0.25; // nice default value
712         }
713
714         self.count = self.cnt;
715         self.mins = '0 0 0';
716         self.maxs = '0 0 0';
717         self.velocity = '0 0 -1';
718         self.mdl = "TE_SPARK";
719         self.impulse = 10 * self.wait; // by default 2.5/sec
720         self.wait = 0;
721         self.cnt = 0; // use mdl
722
723         spawnfunc_func_pointparticles();
724 }
725
726 float rainsnow_SendEntity(entity to, float sf)
727 {
728         WriteByte(MSG_ENTITY, ENT_CLIENT_RAINSNOW);
729         WriteByte(MSG_ENTITY, self.state);
730         WriteCoord(MSG_ENTITY, self.origin_x + self.mins_x);
731         WriteCoord(MSG_ENTITY, self.origin_y + self.mins_y);
732         WriteCoord(MSG_ENTITY, self.origin_z + self.mins_z);
733         WriteCoord(MSG_ENTITY, self.maxs_x - self.mins_x);
734         WriteCoord(MSG_ENTITY, self.maxs_y - self.mins_y);
735         WriteCoord(MSG_ENTITY, self.maxs_z - self.mins_z);
736         WriteShort(MSG_ENTITY, compressShortVector(self.dest));
737         WriteShort(MSG_ENTITY, self.count);
738         WriteByte(MSG_ENTITY, self.cnt);
739         return 1;
740 };
741
742 /*QUAKED spawnfunc_func_rain (0 .5 .8) ?
743 This is an invisible area like a trigger, which rain falls inside of.
744
745 Keys:
746 "velocity"
747  falling direction (should be something like '0 0 -700', use the X and Y velocity for wind)
748 "cnt"
749  sets color of rain (default 12 - white)
750 "count"
751  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
752 */
753 void spawnfunc_func_rain()
754 {
755         self.dest = self.velocity;
756         self.velocity = '0 0 0';
757         if (!self.dest)
758                 self.dest = '0 0 -700';
759         self.angles = '0 0 0';
760         self.movetype = MOVETYPE_NONE;
761         self.solid = SOLID_NOT;
762         SetBrushEntityModel();
763         if (!self.cnt)
764                 self.cnt = 12;
765         if (!self.count)
766                 self.count = 2000;
767         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
768         if (self.count < 1)
769                 self.count = 1;
770         if(self.count > 65535)
771                 self.count = 65535;
772
773         self.state = 1; // 1 is rain, 0 is snow
774         self.Version = 1;
775
776         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
777 };
778
779
780 /*QUAKED spawnfunc_func_snow (0 .5 .8) ?
781 This is an invisible area like a trigger, which snow falls inside of.
782
783 Keys:
784 "velocity"
785  falling direction (should be something like '0 0 -300', use the X and Y velocity for wind)
786 "cnt"
787  sets color of rain (default 12 - white)
788 "count"
789  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
790 */
791 void spawnfunc_func_snow()
792 {
793         self.dest = self.velocity;
794         self.velocity = '0 0 0';
795         if (!self.dest)
796                 self.dest = '0 0 -300';
797         self.angles = '0 0 0';
798         self.movetype = MOVETYPE_NONE;
799         self.solid = SOLID_NOT;
800         SetBrushEntityModel();
801         if (!self.cnt)
802                 self.cnt = 12;
803         if (!self.count)
804                 self.count = 2000;
805         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
806         if (self.count < 1)
807                 self.count = 1;
808         if(self.count > 65535)
809                 self.count = 65535;
810
811         self.state = 0; // 1 is rain, 0 is snow
812         self.Version = 1;
813
814         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
815 };
816
817
818 void FireRailgunBullet (vector start, vector end, float bdamage, float bforce, float deathtype);
819
820 .float modelscale;
821 void misc_laser_aim()
822 {
823         vector a;
824         if(self.enemy)
825         {
826                 if(self.spawnflags & 2)
827                 {
828                         if(self.enemy.origin != self.mangle)
829                         {
830                                 self.mangle = self.enemy.origin;
831                                 self.SendFlags |= 2;
832                         }
833                 }
834                 else
835                 {
836                         a = vectoangles(self.enemy.origin - self.origin);
837                         a_x = -a_x;
838                         if(a != self.mangle)
839                         {
840                                 self.mangle = a;
841                                 self.SendFlags |= 2;
842                         }
843                 }
844         }
845         else
846         {
847                 if(self.angles != self.mangle)
848                 {
849                         self.mangle = self.angles;
850                         self.SendFlags |= 2;
851                 }
852         }
853         if(self.origin != self.oldorigin)
854         {
855                 self.SendFlags |= 1;
856                 self.oldorigin = self.origin;
857         }
858 }
859
860 void misc_laser_init()
861 {
862         if(self.target != "")
863                 self.enemy = find(world, targetname, self.target);
864 }
865
866 .entity pusher;
867 void misc_laser_think()
868 {
869         vector o;
870         entity oldself;
871
872         self.nextthink = time;
873
874         if(!self.state)
875                 return;
876
877         misc_laser_aim();
878
879         if(self.enemy)
880         {
881                 o = self.enemy.origin;
882                 if not(self.spawnflags & 2)
883                         o = self.origin + normalize(o - self.origin) * 32768;
884         }
885         else
886         {
887                 makevectors(self.mangle);
888                 o = self.origin + v_forward * 32768;
889         }
890
891         if(self.dmg)
892         {
893                 if(self.dmg < 0)
894                         FireRailgunBullet(self.origin, o, 100000, 0, DEATH_HURTTRIGGER);
895                 else
896                         FireRailgunBullet(self.origin, o, self.dmg * frametime, 0, DEATH_HURTTRIGGER);
897         }
898
899         if(self.enemy.target != "") // DETECTOR laser
900         {
901                 traceline(self.origin, o, MOVE_NORMAL, self);
902                 if(trace_ent.iscreature)
903                 {
904                         self.pusher = trace_ent;
905                         if(!self.count)
906                         {
907                                 self.count = 1;
908
909                                 oldself = self;
910                                 self = self.enemy;
911                                 activator = self.pusher;
912                                 SUB_UseTargets();
913                                 self = oldself;
914                         }
915                 }
916                 else
917                 {
918                         if(self.count)
919                         {
920                                 self.count = 0;
921
922                                 oldself = self;
923                                 self = self.enemy;
924                                 activator = self.pusher;
925                                 SUB_UseTargets();
926                                 self = oldself;
927                         }
928                 }
929         }
930 }
931
932 float laser_SendEntity(entity to, float fl)
933 {
934         WriteByte(MSG_ENTITY, ENT_CLIENT_LASER);
935         fl = fl - (fl & 0xE0); // use that bit to indicate finite length laser
936         if(self.spawnflags & 2)
937                 fl |= 0x80;
938         if(self.alpha)
939                 fl |= 0x40;
940         if(self.scale != 1 || self.modelscale != 1)
941                 fl |= 0x20;
942         WriteByte(MSG_ENTITY, fl);
943         if(fl & 1)
944         {
945                 WriteCoord(MSG_ENTITY, self.origin_x);
946                 WriteCoord(MSG_ENTITY, self.origin_y);
947                 WriteCoord(MSG_ENTITY, self.origin_z);
948         }
949         if(fl & 8)
950         {
951                 WriteByte(MSG_ENTITY, self.colormod_x * 255.0);
952                 WriteByte(MSG_ENTITY, self.colormod_y * 255.0);
953                 WriteByte(MSG_ENTITY, self.colormod_z * 255.0);
954                 if(fl & 0x40)
955                         WriteByte(MSG_ENTITY, self.alpha * 255.0);
956                 if(fl & 0x20)
957                 {
958                         WriteByte(MSG_ENTITY, bound(0, self.scale * 16.0, 255));
959                         WriteByte(MSG_ENTITY, bound(0, self.modelscale * 16.0, 255));
960                 }
961                 WriteShort(MSG_ENTITY, self.cnt + 1);
962         }
963         if(fl & 2)
964         {
965                 if(fl & 0x80)
966                 {
967                         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
968                         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
969                         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
970                 }
971                 else
972                 {
973                         WriteAngle(MSG_ENTITY, self.mangle_x);
974                         WriteAngle(MSG_ENTITY, self.mangle_y);
975                 }
976         }
977         if(fl & 4)
978                 WriteByte(MSG_ENTITY, self.state);
979         return 1;
980 }
981
982 /*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
983 Any object touching the beam will be hurt
984 Keys:
985 "target"
986  spawnfunc_target_position where the laser ends
987 "mdl"
988  name of beam end effect to use
989 "colormod"
990  color of the beam (default: red)
991 "dmg"
992  damage per second (-1 for a laser that kills immediately)
993 */
994 void laser_use()
995 {
996         self.state = !self.state;
997         self.SendFlags |= 4;
998         misc_laser_aim();
999 }
1000
1001 void laser_reset()
1002 {
1003         if(self.spawnflags & 1)
1004                 self.state = 1;
1005         else
1006                 self.state = 0;
1007 }
1008
1009 void spawnfunc_misc_laser()
1010 {
1011         if(self.mdl)
1012         {
1013                 if(self.mdl == "none")
1014                         self.cnt = -1;
1015                 else
1016                 {
1017                         self.cnt = particleeffectnum(self.mdl);
1018                         if(self.cnt < 0)
1019                                 if(self.dmg)
1020                                         self.cnt = particleeffectnum("laser_deadly");
1021                 }
1022         }
1023         else if(!self.cnt)
1024         {
1025                 if(self.dmg)
1026                         self.cnt = particleeffectnum("laser_deadly");
1027                 else
1028                         self.cnt = -1;
1029         }
1030         if(self.cnt < 0)
1031                 self.cnt = -1;
1032
1033         if(self.colormod == '0 0 0')
1034                 if(!self.alpha)
1035                         self.colormod = '1 0 0';
1036         if(!self.message)
1037                 self.message = "saw the light";
1038         if (!self.message2)
1039                 self.message2 = "was pushed into a laser by";
1040         if(!self.scale)
1041                 self.scale = 1;
1042         if(!self.modelscale)
1043                 self.modelscale = 1;
1044         self.think = misc_laser_think;
1045         self.nextthink = time;
1046         InitializeEntity(self, misc_laser_init, INITPRIO_FINDTARGET);
1047
1048         self.mangle = self.angles;
1049
1050         Net_LinkEntity(self, FALSE, 0, laser_SendEntity);
1051
1052         IFTARGETED
1053         {
1054                 self.reset = laser_reset;
1055                 laser_reset();
1056                 self.use = laser_use;
1057         }
1058         else
1059                 self.state = 1;
1060 }
1061
1062 // tZorks trigger impulse / gravity
1063 .float radius;
1064 .float falloff;
1065 .float strength;
1066 .float lastpushtime;
1067
1068 // targeted (directional) mode
1069 void trigger_impulse_touch1()
1070 {
1071         entity targ;
1072     float pushdeltatime;
1073     float str;
1074
1075         // FIXME: Better checking for what to push and not.
1076         if not(other.iscreature)
1077         if (other.classname != "corpse")
1078         if (other.classname != "body")
1079         if (other.classname != "gib")
1080         if (other.classname != "missile")
1081         if (other.classname != "rocket")
1082         if (other.classname != "casing")
1083         if (other.classname != "grenade")
1084         if (other.classname != "plasma")
1085         if (other.classname != "plasma_prim")
1086         if (other.classname != "plasma_chain")
1087         if (other.classname != "droppedweapon")
1088                 return;
1089
1090         if (other.deadflag && other.iscreature)
1091                 return;
1092
1093         EXACTTRIGGER_TOUCH;
1094
1095     targ = find(world, targetname, self.target);
1096     if(!targ)
1097     {
1098         objerror("trigger_force without a (valid) .target!\n");
1099         remove(self);
1100         return;
1101     }
1102
1103     if(self.falloff == 1)
1104         str = (str / self.radius) * self.strength;
1105     else if(self.falloff == 2)
1106         str = (1 - (str / self.radius)) * self.strength;
1107     else
1108         str = self.strength;
1109
1110     pushdeltatime = time - other.lastpushtime;
1111     if (pushdeltatime > 0.15) pushdeltatime = 0;
1112     other.lastpushtime = time;
1113     if(!pushdeltatime) return;
1114
1115     other.velocity = other.velocity + normalize(targ.origin - self.origin) * self.strength * pushdeltatime;
1116         other.flags &~= FL_ONGROUND;
1117 }
1118
1119 // Directionless (accelerator/decelerator) mode
1120 void trigger_impulse_touch2()
1121 {
1122     float pushdeltatime;
1123
1124         // FIXME: Better checking for what to push and not.
1125         if not(other.iscreature)
1126         if (other.classname != "corpse")
1127         if (other.classname != "body")
1128         if (other.classname != "gib")
1129         if (other.classname != "missile")
1130         if (other.classname != "rocket")
1131         if (other.classname != "casing")
1132         if (other.classname != "grenade")
1133         if (other.classname != "plasma")
1134         if (other.classname != "plasma_prim")
1135         if (other.classname != "plasma_chain")
1136         if (other.classname != "droppedweapon")
1137                 return;
1138
1139         if (other.deadflag && other.iscreature)
1140                 return;
1141
1142         EXACTTRIGGER_TOUCH;
1143
1144     pushdeltatime = time - other.lastpushtime;
1145     if (pushdeltatime > 0.15) pushdeltatime = 0;
1146     other.lastpushtime = time;
1147     if(!pushdeltatime) return;
1148
1149     //if(self.strength > 1)
1150         other.velocity = other.velocity * (self.strength * pushdeltatime);
1151     //else
1152     //    other.velocity = other.velocity - (other.velocity * self.strength * pushdeltatime);
1153 }
1154
1155 // Spherical (gravity/repulsor) mode
1156 void trigger_impulse_touch3()
1157 {
1158     float pushdeltatime;
1159     float str;
1160
1161         // FIXME: Better checking for what to push and not.
1162         if not(other.iscreature)
1163         if (other.classname != "corpse")
1164         if (other.classname != "body")
1165         if (other.classname != "gib")
1166         if (other.classname != "missile")
1167         if (other.classname != "rocket")
1168         if (other.classname != "casing")
1169         if (other.classname != "grenade")
1170         if (other.classname != "plasma")
1171         if (other.classname != "plasma_prim")
1172         if (other.classname != "plasma_chain")
1173         if (other.classname != "droppedweapon")
1174                 return;
1175
1176         if (other.deadflag && other.iscreature)
1177                 return;
1178
1179         EXACTTRIGGER_TOUCH;
1180
1181     pushdeltatime = time - other.lastpushtime;
1182     if (pushdeltatime > 0.15) pushdeltatime = 0;
1183     other.lastpushtime = time;
1184     if(!pushdeltatime) return;
1185
1186     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1187
1188         str = min(self.radius, vlen(self.origin - other.origin));
1189
1190     if(self.falloff == 1)
1191         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1192     else if(self.falloff == 2)
1193         str = (str / self.radius) * self.strength; // 0 in the inside
1194     else
1195         str = self.strength;
1196
1197     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1198 }
1199
1200 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1201 -------- KEYS --------
1202 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1203          If not, this trigger acts like a damper/accelerator field.
1204
1205 strength : This is how mutch force to add in the direction of .target each second
1206            when .target is set. If not, this is hoe mutch to slow down/accelerate
1207            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1208
1209 radius   : If set, act as a spherical device rather then a liniar one.
1210
1211 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1212
1213 -------- NOTES --------
1214 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1215 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1216 */
1217
1218 void spawnfunc_trigger_impulse()
1219 {
1220         EXACTTRIGGER_INIT;
1221     if(self.radius)
1222     {
1223         if(!self.strength) self.strength = 2000;
1224         setorigin(self, self.origin);
1225         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1226         self.touch = trigger_impulse_touch3;
1227     }
1228     else
1229     {
1230         if(self.target)
1231         {
1232             if(!self.strength) self.strength = 950;
1233             self.touch = trigger_impulse_touch1;
1234         }
1235         else
1236         {
1237             if(!self.strength) self.strength = 0.9;
1238             self.touch = trigger_impulse_touch2;
1239         }
1240     }
1241 }
1242
1243 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1244 "Flip-flop" trigger gate... lets only every second trigger event through
1245 */
1246 void flipflop_use()
1247 {
1248         self.state = !self.state;
1249         if(self.state)
1250                 SUB_UseTargets();
1251 }
1252
1253 void spawnfunc_trigger_flipflop()
1254 {
1255         if(self.spawnflags & 1)
1256                 self.state = 1;
1257         self.use = flipflop_use;
1258         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1259 }
1260
1261 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1262 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1263 */
1264 void monoflop_use()
1265 {
1266         self.nextthink = time + self.wait;
1267         self.enemy = activator;
1268         if(self.state)
1269                 return;
1270         self.state = 1;
1271         SUB_UseTargets();
1272 }
1273 void monoflop_fixed_use()
1274 {
1275         if(self.state)
1276                 return;
1277         self.nextthink = time + self.wait;
1278         self.state = 1;
1279         self.enemy = activator;
1280         SUB_UseTargets();
1281 }
1282
1283 void monoflop_think()
1284 {
1285         self.state = 0;
1286         activator = self.enemy;
1287         SUB_UseTargets();
1288 }
1289
1290 void monoflop_reset()
1291 {
1292         self.state = 0;
1293         self.nextthink = 0;
1294 }
1295
1296 void spawnfunc_trigger_monoflop()
1297 {
1298         if(!self.wait)
1299                 self.wait = 1;
1300         if(self.spawnflags & 1)
1301                 self.use = monoflop_fixed_use;
1302         else
1303                 self.use = monoflop_use;
1304         self.think = monoflop_think;
1305         self.state = 0;
1306         self.reset = monoflop_reset;
1307 }
1308
1309 void multivibrator_send()
1310 {
1311         float newstate;
1312         float cyclestart;
1313
1314         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1315
1316         newstate = (time < cyclestart + self.wait);
1317
1318         activator = self;
1319         if(self.state != newstate)
1320                 SUB_UseTargets();
1321         self.state = newstate;
1322
1323         if(self.state)
1324                 self.nextthink = cyclestart + self.wait + 0.01;
1325         else
1326                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1327 }
1328
1329 void multivibrator_toggle()
1330 {
1331         if(self.nextthink == 0)
1332         {
1333                 multivibrator_send();
1334         }
1335         else
1336         {
1337                 if(self.state)
1338                 {
1339                         SUB_UseTargets();
1340                         self.state = 0;
1341                 }
1342                 self.nextthink = 0;
1343         }
1344 }
1345
1346 void multivibrator_reset()
1347 {
1348         if(!(self.spawnflags & 1))
1349                 self.nextthink = 0; // wait for a trigger event
1350         else
1351                 self.nextthink = max(1, time);
1352 }
1353
1354 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1355 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1356 -------- KEYS --------
1357 target: trigger all entities with this targetname when it goes off
1358 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1359 phase: offset of the timing
1360 wait: "on" cycle time (default: 1)
1361 respawntime: "off" cycle time (default: same as wait)
1362 -------- SPAWNFLAGS --------
1363 START_ON: assume it is already turned on (when targeted)
1364 */
1365 void spawnfunc_trigger_multivibrator()
1366 {
1367         if(!self.wait)
1368                 self.wait = 1;
1369         if(!self.respawntime)
1370                 self.respawntime = self.wait;
1371
1372         self.state = 0;
1373         self.use = multivibrator_toggle;
1374         self.think = multivibrator_send;
1375         self.nextthink = time;
1376
1377         IFTARGETED
1378                 multivibrator_reset();
1379 }
1380
1381
1382 void follow_init()
1383 {
1384         entity src, dst;
1385         src = find(world, targetname, self.killtarget);
1386         dst = find(world, targetname, self.target);
1387
1388         if(!src || !dst)
1389         {
1390                 objerror("follow: could not find target/killtarget");
1391                 return;
1392         }
1393
1394         if(self.spawnflags & 1)
1395         {
1396                 // attach
1397                 if(self.spawnflags & 2)
1398                 {
1399                         setattachment(dst, src, self.message);
1400                 }
1401                 else
1402                 {
1403                         attach_sameorigin(dst, src, self.message);
1404                 }
1405         }
1406         else
1407         {
1408                 if(self.spawnflags & 2)
1409                 {
1410                         dst.movetype = MOVETYPE_FOLLOW;
1411                         dst.aiment = src;
1412                         // dst.punchangle = '0 0 0'; // keep unchanged
1413                         dst.view_ofs = dst.origin;
1414                         dst.v_angle = dst.angles;
1415                 }
1416                 else
1417                 {
1418                         follow_sameorigin(dst, src);
1419                 }
1420         }
1421
1422         remove(self);
1423 }
1424
1425 void spawnfunc_misc_follow()
1426 {
1427         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1428 }
1429
1430
1431
1432 void gamestart_use() {
1433         activator = self;
1434         SUB_UseTargets();
1435         remove(self);
1436 }
1437
1438 void spawnfunc_trigger_gamestart() {
1439         self.use = gamestart_use;
1440         self.reset2 = spawnfunc_trigger_gamestart;
1441
1442         if(self.wait)
1443         {
1444                 self.think = self.use;
1445                 self.nextthink = game_starttime + self.wait;
1446         }
1447         else
1448                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1449 }
1450
1451
1452
1453
1454 .entity voicescript; // attached voice script
1455 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1456 .float voicescript_nextthink; // time to play next voice
1457 .float voicescript_voiceend; // time when this voice ends
1458
1459 void target_voicescript_clear(entity pl)
1460 {
1461         pl.voicescript = world;
1462 }
1463
1464 void target_voicescript_use()
1465 {
1466         if(activator.voicescript != self)
1467         {
1468                 activator.voicescript = self;
1469                 activator.voicescript_index = 0;
1470                 activator.voicescript_nextthink = time + self.delay;
1471         }
1472 }
1473
1474 void target_voicescript_next(entity pl)
1475 {
1476         entity vs;
1477         float i, n;
1478
1479         vs = pl.voicescript;
1480         if(!vs)
1481                 return;
1482         if(vs.message == "")
1483                 return;
1484         if(pl.classname != "player")
1485                 return;
1486         if(gameover)
1487                 return;
1488
1489         if(time >= pl.voicescript_voiceend)
1490         {
1491                 if(time >= pl.voicescript_nextthink)
1492                 {
1493                         // get the next voice...
1494                         n = tokenize_sane(vs.message);
1495
1496                         if(pl.voicescript_index < vs.cnt)
1497                                 i = pl.voicescript_index * 2;
1498                         else if(n > vs.cnt * 2)
1499                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1500                         else
1501                                 i = -1;
1502
1503                         if(i >= 0)
1504                         {
1505                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1506                                 pl.voicescript_voiceend = time + stof(argv(i + 1));
1507                         }
1508                         else
1509                                 pl.voicescript = world;
1510
1511                         pl.voicescript_index += 1;
1512                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1513                 }
1514         }
1515 }
1516
1517 void spawnfunc_target_voicescript()
1518 {
1519         // netname: directory of the sound files
1520         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1521         //          foo1 4.1 foo2 4.0 foo3 3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1522         // wait: average time between messages
1523         // delay: initial delay before the first message
1524         
1525         float i, n;
1526         self.use = target_voicescript_use;
1527
1528         n = tokenize_sane(self.message);
1529         self.cnt = n / 2;
1530         for(i = 0; i+1 < n; i += 2)
1531         {
1532                 if(argv(i) == "*")
1533                 {
1534                         self.cnt = i / 2;
1535                         ++i;
1536                 }
1537                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1538         }
1539 }
1540
1541
1542
1543 void trigger_relay_teamcheck_use()
1544 {
1545         if(activator.team)
1546         {
1547                 if(self.spawnflags & 2)
1548                 {
1549                         if(activator.team != self.team)
1550                                 SUB_UseTargets();
1551                 }
1552                 else
1553                 {
1554                         if(activator.team == self.team)
1555                                 SUB_UseTargets();
1556                 }
1557         }
1558         else
1559         {
1560                 if(self.spawnflags & 1)
1561                         SUB_UseTargets();
1562         }
1563 }
1564
1565 void trigger_relay_teamcheck_reset()
1566 {
1567         self.team = self.team_saved;
1568 }
1569
1570 void spawnfunc_trigger_relay_teamcheck()
1571 {
1572         self.team_saved = self.team;
1573         self.use = trigger_relay_teamcheck_use;
1574         self.reset = trigger_relay_teamcheck_reset;
1575 }
1576
1577
1578
1579 void trigger_disablerelay_use()
1580 {
1581         entity e;
1582
1583         float a, b;
1584         a = b = 0;
1585
1586         for(e = world; (e = find(e, targetname, self.target)); )
1587         {
1588                 if(e.use == SUB_UseTargets)
1589                 {
1590                         e.use = SUB_DontUseTargets;
1591                         ++a;
1592                 }
1593                 else if(e.use == SUB_DontUseTargets)
1594                 {
1595                         e.use = SUB_UseTargets;
1596                         ++b;
1597                 }
1598         }
1599
1600         if(!a == !b)
1601                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1602 }
1603
1604 void spawnfunc_trigger_disablerelay()
1605 {
1606         self.use = trigger_disablerelay_use;
1607 }