]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/g_triggers.qc
playerdemo: commands for auto reading/wriitng
[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 mindist, float maxdist, float halflifedist, float forcehalflifedist, 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, 0, 0, 0, 0, DEATH_HURTTRIGGER);
918                 else
919                         FireRailgunBullet(self.origin, o, self.dmg * frametime, 0, 0, 0, 0, 0, DEATH_HURTTRIGGER);
920         }
921
922         if(self.enemy.target != "") // DETECTOR laser
923         {
924                 traceline(self.origin, o, MOVE_NORMAL, self);
925                 if(trace_ent.iscreature)
926                 {
927                         self.pusher = trace_ent;
928                         if(!self.count)
929                         {
930                                 self.count = 1;
931
932                                 oldself = self;
933                                 self = self.enemy;
934                                 activator = self.pusher;
935                                 SUB_UseTargets();
936                                 self = oldself;
937                         }
938                 }
939                 else
940                 {
941                         if(self.count)
942                         {
943                                 self.count = 0;
944
945                                 oldself = self;
946                                 self = self.enemy;
947                                 activator = self.pusher;
948                                 SUB_UseTargets();
949                                 self = oldself;
950                         }
951                 }
952         }
953 }
954
955 float laser_SendEntity(entity to, float fl)
956 {
957         WriteByte(MSG_ENTITY, ENT_CLIENT_LASER);
958         fl = fl - (fl & 0xE0); // use that bit to indicate finite length laser
959         if(self.spawnflags & 2)
960                 fl |= 0x80;
961         if(self.alpha)
962                 fl |= 0x40;
963         if(self.scale != 1 || self.modelscale != 1)
964                 fl |= 0x20;
965         WriteByte(MSG_ENTITY, fl);
966         if(fl & 1)
967         {
968                 WriteCoord(MSG_ENTITY, self.origin_x);
969                 WriteCoord(MSG_ENTITY, self.origin_y);
970                 WriteCoord(MSG_ENTITY, self.origin_z);
971         }
972         if(fl & 8)
973         {
974                 WriteByte(MSG_ENTITY, self.colormod_x * 255.0);
975                 WriteByte(MSG_ENTITY, self.colormod_y * 255.0);
976                 WriteByte(MSG_ENTITY, self.colormod_z * 255.0);
977                 if(fl & 0x40)
978                         WriteByte(MSG_ENTITY, self.alpha * 255.0);
979                 if(fl & 0x20)
980                 {
981                         WriteByte(MSG_ENTITY, bound(0, self.scale * 16.0, 255));
982                         WriteByte(MSG_ENTITY, bound(0, self.modelscale * 16.0, 255));
983                 }
984                 WriteShort(MSG_ENTITY, self.cnt + 1);
985         }
986         if(fl & 2)
987         {
988                 if(fl & 0x80)
989                 {
990                         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
991                         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
992                         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
993                 }
994                 else
995                 {
996                         WriteAngle(MSG_ENTITY, self.mangle_x);
997                         WriteAngle(MSG_ENTITY, self.mangle_y);
998                 }
999         }
1000         if(fl & 4)
1001                 WriteByte(MSG_ENTITY, self.state);
1002         return 1;
1003 }
1004
1005 /*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
1006 Any object touching the beam will be hurt
1007 Keys:
1008 "target"
1009  spawnfunc_target_position where the laser ends
1010 "mdl"
1011  name of beam end effect to use
1012 "colormod"
1013  color of the beam (default: red)
1014 "dmg"
1015  damage per second (-1 for a laser that kills immediately)
1016 */
1017 void laser_use()
1018 {
1019         self.state = !self.state;
1020         self.SendFlags |= 4;
1021         misc_laser_aim();
1022 }
1023
1024 void laser_reset()
1025 {
1026         if(self.spawnflags & 1)
1027                 self.state = 1;
1028         else
1029                 self.state = 0;
1030 }
1031
1032 void spawnfunc_misc_laser()
1033 {
1034         if(self.mdl)
1035         {
1036                 if(self.mdl == "none")
1037                         self.cnt = -1;
1038                 else
1039                 {
1040                         self.cnt = particleeffectnum(self.mdl);
1041                         if(self.cnt < 0)
1042                                 if(self.dmg)
1043                                         self.cnt = particleeffectnum("laser_deadly");
1044                 }
1045         }
1046         else if(!self.cnt)
1047         {
1048                 if(self.dmg)
1049                         self.cnt = particleeffectnum("laser_deadly");
1050                 else
1051                         self.cnt = -1;
1052         }
1053         if(self.cnt < 0)
1054                 self.cnt = -1;
1055
1056         if(self.colormod == '0 0 0')
1057                 if(!self.alpha)
1058                         self.colormod = '1 0 0';
1059         if(!self.message)
1060                 self.message = "saw the light";
1061         if (!self.message2)
1062                 self.message2 = "was pushed into a laser by";
1063         if(!self.scale)
1064                 self.scale = 1;
1065         if(!self.modelscale)
1066                 self.modelscale = 1;
1067         self.think = misc_laser_think;
1068         self.nextthink = time;
1069         InitializeEntity(self, misc_laser_init, INITPRIO_FINDTARGET);
1070
1071         self.mangle = self.angles;
1072
1073         Net_LinkEntity(self, FALSE, 0, laser_SendEntity);
1074
1075         IFTARGETED
1076         {
1077                 self.reset = laser_reset;
1078                 laser_reset();
1079                 self.use = laser_use;
1080         }
1081         else
1082                 self.state = 1;
1083 }
1084
1085 // tZorks trigger impulse / gravity
1086 .float radius;
1087 .float falloff;
1088 .float strength;
1089 .float lastpushtime;
1090
1091 // targeted (directional) mode
1092 void trigger_impulse_touch1()
1093 {
1094         entity targ;
1095     float pushdeltatime;
1096     float str;
1097
1098         // FIXME: Better checking for what to push and not.
1099         if not(other.iscreature)
1100         if (other.classname != "corpse")
1101         if (other.classname != "body")
1102         if (other.classname != "gib")
1103         if (other.classname != "missile")
1104         if (other.classname != "rocket")
1105         if (other.classname != "casing")
1106         if (other.classname != "grenade")
1107         if (other.classname != "plasma")
1108         if (other.classname != "plasma_prim")
1109         if (other.classname != "plasma_chain")
1110         if (other.classname != "droppedweapon")
1111         if (other.classname != "nexball_basketball")
1112         if (other.classname != "nexball_football")
1113                 return;
1114
1115         if (other.deadflag && other.iscreature)
1116                 return;
1117
1118         EXACTTRIGGER_TOUCH;
1119
1120     targ = find(world, targetname, self.target);
1121     if(!targ)
1122     {
1123         objerror("trigger_force without a (valid) .target!\n");
1124         remove(self);
1125         return;
1126     }
1127
1128     if(self.falloff == 1)
1129         str = (str / self.radius) * self.strength;
1130     else if(self.falloff == 2)
1131         str = (1 - (str / self.radius)) * self.strength;
1132     else
1133         str = self.strength;
1134
1135     pushdeltatime = time - other.lastpushtime;
1136     if (pushdeltatime > 0.15) pushdeltatime = 0;
1137     other.lastpushtime = time;
1138     if(!pushdeltatime) return;
1139
1140     other.velocity = other.velocity + normalize(targ.origin - self.origin) * str * pushdeltatime;
1141         other.flags &~= FL_ONGROUND;
1142 }
1143
1144 // Directionless (accelerator/decelerator) mode
1145 void trigger_impulse_touch2()
1146 {
1147     float pushdeltatime;
1148
1149         // FIXME: Better checking for what to push and not.
1150         if not(other.iscreature)
1151         if (other.classname != "corpse")
1152         if (other.classname != "body")
1153         if (other.classname != "gib")
1154         if (other.classname != "missile")
1155         if (other.classname != "rocket")
1156         if (other.classname != "casing")
1157         if (other.classname != "grenade")
1158         if (other.classname != "plasma")
1159         if (other.classname != "plasma_prim")
1160         if (other.classname != "plasma_chain")
1161         if (other.classname != "droppedweapon")
1162         if (other.classname != "nexball_basketball")
1163         if (other.classname != "nexball_football")
1164                 return;
1165
1166         if (other.deadflag && other.iscreature)
1167                 return;
1168
1169         EXACTTRIGGER_TOUCH;
1170
1171     pushdeltatime = time - other.lastpushtime;
1172     if (pushdeltatime > 0.15) pushdeltatime = 0;
1173     other.lastpushtime = time;
1174     if(!pushdeltatime) return;
1175
1176     // div0: ticrate independent, 1 = identity (not 20)
1177     other.velocity = other.velocity * pow(self.strength, pushdeltatime);
1178 }
1179
1180 // Spherical (gravity/repulsor) mode
1181 void trigger_impulse_touch3()
1182 {
1183     float pushdeltatime;
1184     float str;
1185
1186         // FIXME: Better checking for what to push and not.
1187         if not(other.iscreature)
1188         if (other.classname != "corpse")
1189         if (other.classname != "body")
1190         if (other.classname != "gib")
1191         if (other.classname != "missile")
1192         if (other.classname != "rocket")
1193         if (other.classname != "casing")
1194         if (other.classname != "grenade")
1195         if (other.classname != "plasma")
1196         if (other.classname != "plasma_prim")
1197         if (other.classname != "plasma_chain")
1198         if (other.classname != "droppedweapon")
1199         if (other.classname != "nexball_basketball")
1200         if (other.classname != "nexball_football")
1201                 return;
1202
1203         if (other.deadflag && other.iscreature)
1204                 return;
1205
1206         EXACTTRIGGER_TOUCH;
1207
1208     pushdeltatime = time - other.lastpushtime;
1209     if (pushdeltatime > 0.15) pushdeltatime = 0;
1210     other.lastpushtime = time;
1211     if(!pushdeltatime) return;
1212
1213     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1214
1215         str = min(self.radius, vlen(self.origin - other.origin));
1216
1217     if(self.falloff == 1)
1218         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1219     else if(self.falloff == 2)
1220         str = (str / self.radius) * self.strength; // 0 in the inside
1221     else
1222         str = self.strength;
1223
1224     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1225 }
1226
1227 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1228 -------- KEYS --------
1229 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1230          If not, this trigger acts like a damper/accelerator field.
1231
1232 strength : This is how mutch force to add in the direction of .target each second
1233            when .target is set. If not, this is hoe mutch to slow down/accelerate
1234            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1235
1236 radius   : If set, act as a spherical device rather then a liniar one.
1237
1238 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1239
1240 -------- NOTES --------
1241 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1242 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1243 */
1244
1245 void spawnfunc_trigger_impulse()
1246 {
1247         EXACTTRIGGER_INIT;
1248     if(self.radius)
1249     {
1250         if(!self.strength) self.strength = 2000 * cvar("g_triggerimpulse_radial_multiplier");
1251         setorigin(self, self.origin);
1252         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1253         self.touch = trigger_impulse_touch3;
1254     }
1255     else
1256     {
1257         if(self.target)
1258         {
1259             if(!self.strength) self.strength = 950 * cvar("g_triggerimpulse_directional_multiplier");
1260             self.touch = trigger_impulse_touch1;
1261         }
1262         else
1263         {
1264             if(!self.strength) self.strength = 0.9;
1265                         self.strength = pow(self.strength, cvar("g_triggerimpulse_accel_power")) * cvar("g_triggerimpulse_accel_multiplier");
1266             self.touch = trigger_impulse_touch2;
1267         }
1268     }
1269 }
1270
1271 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1272 "Flip-flop" trigger gate... lets only every second trigger event through
1273 */
1274 void flipflop_use()
1275 {
1276         self.state = !self.state;
1277         if(self.state)
1278                 SUB_UseTargets();
1279 }
1280
1281 void spawnfunc_trigger_flipflop()
1282 {
1283         if(self.spawnflags & 1)
1284                 self.state = 1;
1285         self.use = flipflop_use;
1286         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1287 }
1288
1289 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1290 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1291 */
1292 void monoflop_use()
1293 {
1294         self.nextthink = time + self.wait;
1295         self.enemy = activator;
1296         if(self.state)
1297                 return;
1298         self.state = 1;
1299         SUB_UseTargets();
1300 }
1301 void monoflop_fixed_use()
1302 {
1303         if(self.state)
1304                 return;
1305         self.nextthink = time + self.wait;
1306         self.state = 1;
1307         self.enemy = activator;
1308         SUB_UseTargets();
1309 }
1310
1311 void monoflop_think()
1312 {
1313         self.state = 0;
1314         activator = self.enemy;
1315         SUB_UseTargets();
1316 }
1317
1318 void monoflop_reset()
1319 {
1320         self.state = 0;
1321         self.nextthink = 0;
1322 }
1323
1324 void spawnfunc_trigger_monoflop()
1325 {
1326         if(!self.wait)
1327                 self.wait = 1;
1328         if(self.spawnflags & 1)
1329                 self.use = monoflop_fixed_use;
1330         else
1331                 self.use = monoflop_use;
1332         self.think = monoflop_think;
1333         self.state = 0;
1334         self.reset = monoflop_reset;
1335 }
1336
1337 void multivibrator_send()
1338 {
1339         float newstate;
1340         float cyclestart;
1341
1342         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1343
1344         newstate = (time < cyclestart + self.wait);
1345
1346         activator = self;
1347         if(self.state != newstate)
1348                 SUB_UseTargets();
1349         self.state = newstate;
1350
1351         if(self.state)
1352                 self.nextthink = cyclestart + self.wait + 0.01;
1353         else
1354                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1355 }
1356
1357 void multivibrator_toggle()
1358 {
1359         if(self.nextthink == 0)
1360         {
1361                 multivibrator_send();
1362         }
1363         else
1364         {
1365                 if(self.state)
1366                 {
1367                         SUB_UseTargets();
1368                         self.state = 0;
1369                 }
1370                 self.nextthink = 0;
1371         }
1372 }
1373
1374 void multivibrator_reset()
1375 {
1376         if(!(self.spawnflags & 1))
1377                 self.nextthink = 0; // wait for a trigger event
1378         else
1379                 self.nextthink = max(1, time);
1380 }
1381
1382 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1383 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1384 -------- KEYS --------
1385 target: trigger all entities with this targetname when it goes off
1386 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1387 phase: offset of the timing
1388 wait: "on" cycle time (default: 1)
1389 respawntime: "off" cycle time (default: same as wait)
1390 -------- SPAWNFLAGS --------
1391 START_ON: assume it is already turned on (when targeted)
1392 */
1393 void spawnfunc_trigger_multivibrator()
1394 {
1395         if(!self.wait)
1396                 self.wait = 1;
1397         if(!self.respawntime)
1398                 self.respawntime = self.wait;
1399
1400         self.state = 0;
1401         self.use = multivibrator_toggle;
1402         self.think = multivibrator_send;
1403         self.nextthink = time;
1404
1405         IFTARGETED
1406                 multivibrator_reset();
1407 }
1408
1409
1410 void follow_init()
1411 {
1412         entity src, dst;
1413         src = world;
1414         dst = world;
1415         if(self.killtarget != "")
1416                 src = find(world, targetname, self.killtarget);
1417         if(self.target != "")
1418                 dst = find(world, targetname, self.target);
1419
1420         if(!src && !dst)
1421         {
1422                 objerror("follow: could not find target/killtarget");
1423                 return;
1424         }
1425
1426         if(self.jointtype)
1427         {
1428                 // already done :P entity must stay
1429                 self.aiment = src;
1430                 self.enemy = dst;
1431         }
1432         else if(!src || !dst)
1433         {
1434                 objerror("follow: could not find target/killtarget");
1435                 return;
1436         }
1437         else if(self.spawnflags & 1)
1438         {
1439                 // attach
1440                 if(self.spawnflags & 2)
1441                 {
1442                         setattachment(dst, src, self.message);
1443                 }
1444                 else
1445                 {
1446                         attach_sameorigin(dst, src, self.message);
1447                 }
1448
1449                 remove(self);
1450         }
1451         else
1452         {
1453                 if(self.spawnflags & 2)
1454                 {
1455                         dst.movetype = MOVETYPE_FOLLOW;
1456                         dst.aiment = src;
1457                         // dst.punchangle = '0 0 0'; // keep unchanged
1458                         dst.view_ofs = dst.origin;
1459                         dst.v_angle = dst.angles;
1460                 }
1461                 else
1462                 {
1463                         follow_sameorigin(dst, src);
1464                 }
1465
1466                 remove(self);
1467         }
1468 }
1469
1470 void spawnfunc_misc_follow()
1471 {
1472         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1473 }
1474
1475
1476
1477 void gamestart_use() {
1478         activator = self;
1479         SUB_UseTargets();
1480         remove(self);
1481 }
1482
1483 void spawnfunc_trigger_gamestart() {
1484         self.use = gamestart_use;
1485         self.reset2 = spawnfunc_trigger_gamestart;
1486
1487         if(self.wait)
1488         {
1489                 self.think = self.use;
1490                 self.nextthink = game_starttime + self.wait;
1491         }
1492         else
1493                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1494 }
1495
1496
1497
1498
1499 .entity voicescript; // attached voice script
1500 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1501 .float voicescript_nextthink; // time to play next voice
1502 .float voicescript_voiceend; // time when this voice ends
1503
1504 void target_voicescript_clear(entity pl)
1505 {
1506         pl.voicescript = world;
1507 }
1508
1509 void target_voicescript_use()
1510 {
1511         if(activator.voicescript != self)
1512         {
1513                 activator.voicescript = self;
1514                 activator.voicescript_index = 0;
1515                 activator.voicescript_nextthink = time + self.delay;
1516         }
1517 }
1518
1519 void target_voicescript_next(entity pl)
1520 {
1521         entity vs;
1522         float i, n, dt;
1523
1524         vs = pl.voicescript;
1525         if(!vs)
1526                 return;
1527         if(vs.message == "")
1528                 return;
1529         if(pl.classname != "player")
1530                 return;
1531         if(gameover)
1532                 return;
1533
1534         if(time >= pl.voicescript_voiceend)
1535         {
1536                 if(time >= pl.voicescript_nextthink)
1537                 {
1538                         // get the next voice...
1539                         n = tokenize_console(vs.message);
1540
1541                         if(pl.voicescript_index < vs.cnt)
1542                                 i = pl.voicescript_index * 2;
1543                         else if(n > vs.cnt * 2)
1544                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1545                         else
1546                                 i = -1;
1547
1548                         if(i >= 0)
1549                         {
1550                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1551                                 dt = stof(argv(i + 1));
1552                                 if(dt >= 0)
1553                                 {
1554                                         pl.voicescript_voiceend = time + dt;
1555                                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1556                                 }
1557                                 else
1558                                 {
1559                                         pl.voicescript_voiceend = time - dt;
1560                                         pl.voicescript_nextthink = pl.voicescript_voiceend;
1561                                 }
1562
1563                                 pl.voicescript_index += 1;
1564                         }
1565                         else
1566                         {
1567                                 pl.voicescript = world; // stop trying then
1568                         }
1569                 }
1570         }
1571 }
1572
1573 void spawnfunc_target_voicescript()
1574 {
1575         // netname: directory of the sound files
1576         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1577         //          foo1 4.1 foo2 4.0 foo3 -3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1578         //          Here, a - in front of the duration means that no delay is to be
1579         //          added after this message
1580         // wait: average time between messages
1581         // delay: initial delay before the first message
1582         
1583         float i, n;
1584         self.use = target_voicescript_use;
1585
1586         n = tokenize_console(self.message);
1587         self.cnt = n / 2;
1588         for(i = 0; i+1 < n; i += 2)
1589         {
1590                 if(argv(i) == "*")
1591                 {
1592                         self.cnt = i / 2;
1593                         ++i;
1594                 }
1595                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1596         }
1597 }
1598
1599
1600
1601 void trigger_relay_teamcheck_use()
1602 {
1603         if(activator.team)
1604         {
1605                 if(self.spawnflags & 2)
1606                 {
1607                         if(activator.team != self.team)
1608                                 SUB_UseTargets();
1609                 }
1610                 else
1611                 {
1612                         if(activator.team == self.team)
1613                                 SUB_UseTargets();
1614                 }
1615         }
1616         else
1617         {
1618                 if(self.spawnflags & 1)
1619                         SUB_UseTargets();
1620         }
1621 }
1622
1623 void trigger_relay_teamcheck_reset()
1624 {
1625         self.team = self.team_saved;
1626 }
1627
1628 void spawnfunc_trigger_relay_teamcheck()
1629 {
1630         self.team_saved = self.team;
1631         self.use = trigger_relay_teamcheck_use;
1632         self.reset = trigger_relay_teamcheck_reset;
1633 }
1634
1635
1636
1637 void trigger_disablerelay_use()
1638 {
1639         entity e;
1640
1641         float a, b;
1642         a = b = 0;
1643
1644         for(e = world; (e = find(e, targetname, self.target)); )
1645         {
1646                 if(e.use == SUB_UseTargets)
1647                 {
1648                         e.use = SUB_DontUseTargets;
1649                         ++a;
1650                 }
1651                 else if(e.use == SUB_DontUseTargets)
1652                 {
1653                         e.use = SUB_UseTargets;
1654                         ++b;
1655                 }
1656         }
1657
1658         if((!a) == (!b))
1659                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1660 }
1661
1662 void spawnfunc_trigger_disablerelay()
1663 {
1664         self.use = trigger_disablerelay_use;
1665 }
1666
1667 float magicear_matched;
1668 string trigger_magicear_processmessage(entity ear, entity source, float teamsay, entity privatesay, string msgin)
1669 {
1670         float domatch, dotrigger, matchstart, l;
1671         string s, msg;
1672         entity oldself;
1673
1674         magicear_matched = FALSE;
1675
1676         dotrigger = ((self.classname == "player") && (self.deadflag == DEAD_NO) && ((ear.radius == 0) || (vlen(source.origin - ear.origin) <= ear.radius)));
1677         domatch = ((ear.spawnflags & 32) || dotrigger);
1678         if not(domatch)
1679                 return msgin;
1680
1681         if(privatesay)
1682         {
1683                 if(ear.spawnflags & 4)
1684                         return msgin;
1685         }
1686         else
1687         {
1688                 if(!teamsay)
1689                         if(ear.spawnflags & 1)
1690                                 return msgin;
1691                 if(teamsay > 0)
1692                         if(ear.spawnflags & 2)
1693                                 return msgin;
1694                 if(teamsay < 0)
1695                         if(ear.spawnflags & 8)
1696                                 return msgin;
1697         }
1698         
1699         matchstart = -1;
1700         l = strlen(ear.message);
1701
1702         if(self.spawnflags & 128)
1703                 msg = msgin;
1704         else
1705                 msg = strdecolorize(msgin);
1706
1707         if(substring(ear.message, 0, 1) == "*")
1708         {
1709                 if(substring(ear.message, -1, 1) == "*")
1710                 {
1711                         // two wildcards
1712                         // as we need multi-replacement here...
1713                         s = substring(ear.message, 1, -2);
1714                         l -= 2;
1715                         if(strstrofs(msg, s, 0) >= 0)
1716                                 matchstart = -2; // we use strreplace on s
1717                 }
1718                 else
1719                 {
1720                         // match at start
1721                         s = substring(ear.message, 1, -1);
1722                         l -= 1;
1723                         if(substring(msg, -l, l) == s)
1724                                 matchstart = strlen(msg) - l;
1725                 }
1726         }
1727         else
1728         {
1729                 if(substring(ear.message, -1, 1) == "*")
1730                 {
1731                         // match at end
1732                         s = substring(ear.message, 0, -2);
1733                         l -= 1;
1734                         if(substring(msg, 0, l) == s)
1735                                 matchstart = 0;
1736                 }
1737                 else
1738                 {
1739                         // full match
1740                         s = ear.message;
1741                         if(msg == ear.message)
1742                                 matchstart = 0;
1743                 }
1744         }
1745
1746         if(matchstart == -1) // no match
1747                 return msgin;
1748
1749         magicear_matched = TRUE;
1750
1751         if(dotrigger)
1752         {
1753                 oldself = activator = self;
1754                 self = ear;
1755                 SUB_UseTargets();
1756                 self = oldself;
1757         }
1758
1759         if(ear.spawnflags & 16)
1760         {
1761                 return ear.netname;
1762         }
1763         else if(ear.netname != "")
1764         {
1765                 if(matchstart < 0)
1766                         return strreplace(s, ear.netname, msg);
1767                 else
1768                         return strcat(
1769                                 substring(msg, 0, matchstart),
1770                                 ear.netname,
1771                                 substring(msg, matchstart + l, -1)
1772                         );
1773         }
1774         else
1775                 return msgin;
1776 }
1777
1778 entity magicears;
1779 string trigger_magicear_processmessage_forallears(entity source, float teamsay, entity privatesay, string msgin)
1780 {
1781         entity ear;
1782         string msgout;
1783         for(ear = magicears; ear; ear = ear.enemy)
1784         {
1785                 msgout = trigger_magicear_processmessage(ear, source, teamsay, privatesay, msgin);
1786                 if not(ear.spawnflags & 64)
1787                         if(magicear_matched)
1788                                 return msgout;
1789                 msgin = msgout;
1790         }
1791         return msgin;
1792 }
1793
1794 void spawnfunc_trigger_magicear()
1795 {
1796         self.enemy = magicears;
1797         magicears = self;
1798
1799         // actually handled in "say" processing
1800         // spawnflags:
1801         //   1 = ignore say
1802         //   2 = ignore teamsay
1803         //   4 = ignore tell
1804         //   8 = ignore tell to unknown player
1805         //   16 = let netname replace the whole message (otherwise, netname is a word replacement if set)
1806         //   32 = perform the replacement even if outside the radius or dead
1807         //   64 = continue replacing/triggering even if this one matched
1808         // message: either
1809         //   *pattern*
1810         // or
1811         //   *pattern
1812         // or
1813         //   pattern*
1814         // or
1815         //   pattern
1816         // netname:
1817         //   if set, replacement for the matched text
1818         // radius:
1819         //   "hearing distance"
1820         // target:
1821         //   what to trigger
1822 }