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