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