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