]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/g_triggers.qc
fixed a bug which prevented falloff modes from working in directional
[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     // LordHavoc: rewrote this velocity math (was * self.strength * pushdeltatime) to make it less ticrate dependent
1177     //if(self.strength > 1)
1178         other.velocity = other.velocity * (1 + (self.strength - 20) * pushdeltatime);
1179     //else
1180     //    other.velocity = other.velocity - (other.velocity * self.strength * pushdeltatime);
1181 }
1182
1183 // Spherical (gravity/repulsor) mode
1184 void trigger_impulse_touch3()
1185 {
1186     float pushdeltatime;
1187     float str;
1188
1189         // FIXME: Better checking for what to push and not.
1190         if not(other.iscreature)
1191         if (other.classname != "corpse")
1192         if (other.classname != "body")
1193         if (other.classname != "gib")
1194         if (other.classname != "missile")
1195         if (other.classname != "rocket")
1196         if (other.classname != "casing")
1197         if (other.classname != "grenade")
1198         if (other.classname != "plasma")
1199         if (other.classname != "plasma_prim")
1200         if (other.classname != "plasma_chain")
1201         if (other.classname != "droppedweapon")
1202         if (other.classname != "nexball_basketball")
1203         if (other.classname != "nexball_football")
1204                 return;
1205
1206         if (other.deadflag && other.iscreature)
1207                 return;
1208
1209         EXACTTRIGGER_TOUCH;
1210
1211     pushdeltatime = time - other.lastpushtime;
1212     if (pushdeltatime > 0.15) pushdeltatime = 0;
1213     other.lastpushtime = time;
1214     if(!pushdeltatime) return;
1215
1216     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1217
1218         str = min(self.radius, vlen(self.origin - other.origin));
1219
1220     if(self.falloff == 1)
1221         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1222     else if(self.falloff == 2)
1223         str = (str / self.radius) * self.strength; // 0 in the inside
1224     else
1225         str = self.strength;
1226
1227     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1228 }
1229
1230 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1231 -------- KEYS --------
1232 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1233          If not, this trigger acts like a damper/accelerator field.
1234
1235 strength : This is how mutch force to add in the direction of .target each second
1236            when .target is set. If not, this is hoe mutch to slow down/accelerate
1237            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1238
1239 radius   : If set, act as a spherical device rather then a liniar one.
1240
1241 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1242
1243 -------- NOTES --------
1244 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1245 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1246 */
1247
1248 void spawnfunc_trigger_impulse()
1249 {
1250         EXACTTRIGGER_INIT;
1251     if(self.radius)
1252     {
1253         if(!self.strength) self.strength = 2000;
1254         setorigin(self, self.origin);
1255         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1256         self.touch = trigger_impulse_touch3;
1257     }
1258     else
1259     {
1260         if(self.target)
1261         {
1262             if(!self.strength) self.strength = 950;
1263             self.touch = trigger_impulse_touch1;
1264         }
1265         else
1266         {
1267             if(!self.strength) self.strength = 0.9;
1268             self.touch = trigger_impulse_touch2;
1269         }
1270     }
1271 }
1272
1273 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1274 "Flip-flop" trigger gate... lets only every second trigger event through
1275 */
1276 void flipflop_use()
1277 {
1278         self.state = !self.state;
1279         if(self.state)
1280                 SUB_UseTargets();
1281 }
1282
1283 void spawnfunc_trigger_flipflop()
1284 {
1285         if(self.spawnflags & 1)
1286                 self.state = 1;
1287         self.use = flipflop_use;
1288         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1289 }
1290
1291 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1292 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1293 */
1294 void monoflop_use()
1295 {
1296         self.nextthink = time + self.wait;
1297         self.enemy = activator;
1298         if(self.state)
1299                 return;
1300         self.state = 1;
1301         SUB_UseTargets();
1302 }
1303 void monoflop_fixed_use()
1304 {
1305         if(self.state)
1306                 return;
1307         self.nextthink = time + self.wait;
1308         self.state = 1;
1309         self.enemy = activator;
1310         SUB_UseTargets();
1311 }
1312
1313 void monoflop_think()
1314 {
1315         self.state = 0;
1316         activator = self.enemy;
1317         SUB_UseTargets();
1318 }
1319
1320 void monoflop_reset()
1321 {
1322         self.state = 0;
1323         self.nextthink = 0;
1324 }
1325
1326 void spawnfunc_trigger_monoflop()
1327 {
1328         if(!self.wait)
1329                 self.wait = 1;
1330         if(self.spawnflags & 1)
1331                 self.use = monoflop_fixed_use;
1332         else
1333                 self.use = monoflop_use;
1334         self.think = monoflop_think;
1335         self.state = 0;
1336         self.reset = monoflop_reset;
1337 }
1338
1339 void multivibrator_send()
1340 {
1341         float newstate;
1342         float cyclestart;
1343
1344         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1345
1346         newstate = (time < cyclestart + self.wait);
1347
1348         activator = self;
1349         if(self.state != newstate)
1350                 SUB_UseTargets();
1351         self.state = newstate;
1352
1353         if(self.state)
1354                 self.nextthink = cyclestart + self.wait + 0.01;
1355         else
1356                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1357 }
1358
1359 void multivibrator_toggle()
1360 {
1361         if(self.nextthink == 0)
1362         {
1363                 multivibrator_send();
1364         }
1365         else
1366         {
1367                 if(self.state)
1368                 {
1369                         SUB_UseTargets();
1370                         self.state = 0;
1371                 }
1372                 self.nextthink = 0;
1373         }
1374 }
1375
1376 void multivibrator_reset()
1377 {
1378         if(!(self.spawnflags & 1))
1379                 self.nextthink = 0; // wait for a trigger event
1380         else
1381                 self.nextthink = max(1, time);
1382 }
1383
1384 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1385 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1386 -------- KEYS --------
1387 target: trigger all entities with this targetname when it goes off
1388 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1389 phase: offset of the timing
1390 wait: "on" cycle time (default: 1)
1391 respawntime: "off" cycle time (default: same as wait)
1392 -------- SPAWNFLAGS --------
1393 START_ON: assume it is already turned on (when targeted)
1394 */
1395 void spawnfunc_trigger_multivibrator()
1396 {
1397         if(!self.wait)
1398                 self.wait = 1;
1399         if(!self.respawntime)
1400                 self.respawntime = self.wait;
1401
1402         self.state = 0;
1403         self.use = multivibrator_toggle;
1404         self.think = multivibrator_send;
1405         self.nextthink = time;
1406
1407         IFTARGETED
1408                 multivibrator_reset();
1409 }
1410
1411
1412 void follow_init()
1413 {
1414         entity src, dst;
1415         src = find(world, targetname, self.killtarget);
1416         dst = find(world, targetname, self.target);
1417
1418         if(!src || !dst)
1419         {
1420                 objerror("follow: could not find target/killtarget");
1421                 return;
1422         }
1423
1424         if(self.spawnflags & 1)
1425         {
1426                 // attach
1427                 if(self.spawnflags & 2)
1428                 {
1429                         setattachment(dst, src, self.message);
1430                 }
1431                 else
1432                 {
1433                         attach_sameorigin(dst, src, self.message);
1434                 }
1435         }
1436         else
1437         {
1438                 if(self.spawnflags & 2)
1439                 {
1440                         dst.movetype = MOVETYPE_FOLLOW;
1441                         dst.aiment = src;
1442                         // dst.punchangle = '0 0 0'; // keep unchanged
1443                         dst.view_ofs = dst.origin;
1444                         dst.v_angle = dst.angles;
1445                 }
1446                 else
1447                 {
1448                         follow_sameorigin(dst, src);
1449                 }
1450         }
1451
1452         remove(self);
1453 }
1454
1455 void spawnfunc_misc_follow()
1456 {
1457         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1458 }
1459
1460
1461
1462 void gamestart_use() {
1463         activator = self;
1464         SUB_UseTargets();
1465         remove(self);
1466 }
1467
1468 void spawnfunc_trigger_gamestart() {
1469         self.use = gamestart_use;
1470         self.reset2 = spawnfunc_trigger_gamestart;
1471
1472         if(self.wait)
1473         {
1474                 self.think = self.use;
1475                 self.nextthink = game_starttime + self.wait;
1476         }
1477         else
1478                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1479 }
1480
1481
1482
1483
1484 .entity voicescript; // attached voice script
1485 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1486 .float voicescript_nextthink; // time to play next voice
1487 .float voicescript_voiceend; // time when this voice ends
1488
1489 void target_voicescript_clear(entity pl)
1490 {
1491         pl.voicescript = world;
1492 }
1493
1494 void target_voicescript_use()
1495 {
1496         if(activator.voicescript != self)
1497         {
1498                 activator.voicescript = self;
1499                 activator.voicescript_index = 0;
1500                 activator.voicescript_nextthink = time + self.delay;
1501         }
1502 }
1503
1504 void target_voicescript_next(entity pl)
1505 {
1506         entity vs;
1507         float i, n, dt;
1508
1509         vs = pl.voicescript;
1510         if(!vs)
1511                 return;
1512         if(vs.message == "")
1513                 return;
1514         if(pl.classname != "player")
1515                 return;
1516         if(gameover)
1517                 return;
1518
1519         if(time >= pl.voicescript_voiceend)
1520         {
1521                 if(time >= pl.voicescript_nextthink)
1522                 {
1523                         // get the next voice...
1524                         n = tokenize_console(vs.message);
1525
1526                         if(pl.voicescript_index < vs.cnt)
1527                                 i = pl.voicescript_index * 2;
1528                         else if(n > vs.cnt * 2)
1529                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1530                         else
1531                                 i = -1;
1532
1533                         if(i >= 0)
1534                         {
1535                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1536                                 dt = stof(argv(i + 1));
1537                                 if(dt >= 0)
1538                                 {
1539                                         pl.voicescript_voiceend = time + dt;
1540                                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1541                                 }
1542                                 else
1543                                 {
1544                                         pl.voicescript_voiceend = time - dt;
1545                                         pl.voicescript_nextthink = pl.voicescript_voiceend;
1546                                 }
1547
1548                                 pl.voicescript_index += 1;
1549                         }
1550                         else
1551                         {
1552                                 pl.voicescript = world; // stop trying then
1553                         }
1554                 }
1555         }
1556 }
1557
1558 void spawnfunc_target_voicescript()
1559 {
1560         // netname: directory of the sound files
1561         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1562         //          foo1 4.1 foo2 4.0 foo3 -3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1563         //          Here, a - in front of the duration means that no delay is to be
1564         //          added after this message
1565         // wait: average time between messages
1566         // delay: initial delay before the first message
1567         
1568         float i, n;
1569         self.use = target_voicescript_use;
1570
1571         n = tokenize_console(self.message);
1572         self.cnt = n / 2;
1573         for(i = 0; i+1 < n; i += 2)
1574         {
1575                 if(argv(i) == "*")
1576                 {
1577                         self.cnt = i / 2;
1578                         ++i;
1579                 }
1580                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1581         }
1582 }
1583
1584
1585
1586 void trigger_relay_teamcheck_use()
1587 {
1588         if(activator.team)
1589         {
1590                 if(self.spawnflags & 2)
1591                 {
1592                         if(activator.team != self.team)
1593                                 SUB_UseTargets();
1594                 }
1595                 else
1596                 {
1597                         if(activator.team == self.team)
1598                                 SUB_UseTargets();
1599                 }
1600         }
1601         else
1602         {
1603                 if(self.spawnflags & 1)
1604                         SUB_UseTargets();
1605         }
1606 }
1607
1608 void trigger_relay_teamcheck_reset()
1609 {
1610         self.team = self.team_saved;
1611 }
1612
1613 void spawnfunc_trigger_relay_teamcheck()
1614 {
1615         self.team_saved = self.team;
1616         self.use = trigger_relay_teamcheck_use;
1617         self.reset = trigger_relay_teamcheck_reset;
1618 }
1619
1620
1621
1622 void trigger_disablerelay_use()
1623 {
1624         entity e;
1625
1626         float a, b;
1627         a = b = 0;
1628
1629         for(e = world; (e = find(e, targetname, self.target)); )
1630         {
1631                 if(e.use == SUB_UseTargets)
1632                 {
1633                         e.use = SUB_DontUseTargets;
1634                         ++a;
1635                 }
1636                 else if(e.use == SUB_DontUseTargets)
1637                 {
1638                         e.use = SUB_UseTargets;
1639                         ++b;
1640                 }
1641         }
1642
1643         if((!a) == (!b))
1644                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1645 }
1646
1647 void spawnfunc_trigger_disablerelay()
1648 {
1649         self.use = trigger_disablerelay_use;
1650 }
1651
1652 float magicear_matched;
1653 string trigger_magicear_processmessage(entity ear, entity source, float teamsay, entity privatesay, string msgin)
1654 {
1655         float domatch, dotrigger, matchstart, l;
1656         string s;
1657         entity oldself;
1658
1659         magicear_matched = FALSE;
1660
1661         dotrigger = ((self.classname == "player") && ((ear.radius == 0) || (vlen(source.origin - ear.origin) <= ear.radius)));
1662         domatch = ((ear.spawnflags & 32) || dotrigger);
1663         if not(domatch)
1664                 return msgin;
1665
1666         if(privatesay)
1667         {
1668                 if(ear.spawnflags & 4)
1669                         return msgin;
1670         }
1671         else
1672         {
1673                 if(!teamsay)
1674                         if(ear.spawnflags & 1)
1675                                 return msgin;
1676                 if(teamsay > 0)
1677                         if(ear.spawnflags & 2)
1678                                 return msgin;
1679                 if(teamsay < 0)
1680                         if(ear.spawnflags & 8)
1681                                 return msgin;
1682         }
1683         
1684         matchstart = -1;
1685         l = strlen(ear.message);
1686
1687         if(substring(ear.message, 0, 1) == "*")
1688         {
1689                 if(substring(ear.message, -1, 1) == "*")
1690                 {
1691                         // two wildcards
1692                         // as we need multi-replacement here...
1693                         s = substring(ear.message, 1, -2);
1694                         l -= 2;
1695                         if(strstrofs(msgin, s, 0) >= 0)
1696                                 matchstart = -2; // we use strreplace on s
1697                 }
1698                 else
1699                 {
1700                         // match at start
1701                         s = substring(ear.message, 1, -1);
1702                         l -= 1;
1703                         if(substring(msgin, -l, l) == s)
1704                                 matchstart = strlen(msgin) - l;
1705                 }
1706         }
1707         else
1708         {
1709                 if(substring(ear.message, -1, 1) == "*")
1710                 {
1711                         // match at end
1712                         s = substring(ear.message, 0, -2);
1713                         l -= 1;
1714                         if(substring(msgin, 0, l) == s)
1715                                 matchstart = 0;
1716                 }
1717                 else
1718                 {
1719                         // full match
1720                         s = ear.message;
1721                         if(msgin == ear.message)
1722                                 matchstart = 0;
1723                 }
1724         }
1725
1726         if(matchstart == -1) // no match
1727                 return msgin;
1728
1729         magicear_matched = TRUE;
1730
1731         if(dotrigger)
1732         {
1733                 oldself = activator = self;
1734                 self = ear;
1735                 SUB_UseTargets();
1736                 self = oldself;
1737         }
1738
1739         if(ear.spawnflags & 16)
1740         {
1741                 return ear.netname;
1742         }
1743         else if(ear.netname != "")
1744         {
1745                 if(matchstart < 0)
1746                         return strreplace(s, ear.netname, msgin);
1747                 else
1748                         return strcat(
1749                                 substring(msgin, 0, matchstart),
1750                                 ear.netname,
1751                                 substring(msgin, matchstart + l, -1)
1752                         );
1753         }
1754         else
1755                 return msgin;
1756 }
1757
1758 entity magicears;
1759 string trigger_magicear_processmessage_forallears(entity source, float teamsay, entity privatesay, string msgin)
1760 {
1761         entity ear;
1762         string msgout;
1763         for(ear = magicears; ear; ear = ear.enemy)
1764         {
1765                 msgout = trigger_magicear_processmessage(ear, source, teamsay, privatesay, msgin);
1766                 if not(ear.spawnflags & 64)
1767                         if(magicear_matched)
1768                                 return msgout;
1769                 msgin = msgout;
1770         }
1771         return msgin;
1772 }
1773
1774 void spawnfunc_trigger_magicear()
1775 {
1776         self.enemy = magicears;
1777         magicears = self;
1778
1779         // actually handled in "say" processing
1780         // spawnflags:
1781         //   1 = ignore say
1782         //   2 = ignore teamsay
1783         //   4 = ignore tell
1784         //   8 = ignore tell to unknown player
1785         //   16 = let netname replace the whole message (otherwise, netname is a word replacement if set)
1786         //   32 = perform the replacement even if outside the radius
1787         //   64 = continue replacing/triggering even if this one matched
1788         // message: either
1789         //   *pattern*
1790         // or
1791         //   *pattern
1792         // or
1793         //   pattern*
1794         // or
1795         //   pattern
1796         // netname:
1797         //   if set, replacement for the matched text
1798         // radius:
1799         //   "hearing distance"
1800         // target:
1801         //   what to trigger
1802 }