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