]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_triggers.qc
Attempt to implement untouch detection, but I didn't figure the correct maths yet...
[xonotic/xonotic-data.pk3dir.git] / 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 .float triggergravity, triggergravitytime;
537 .entity trigger_gravity_check;
538 void trigger_gravity_check_think()
539 {
540         if(self.owner.triggergravitytime < time + 0.1) // need to figure out a correct formula here
541         {
542                 dprint("XXXXXXXXXXXXXXXXXXXXXXXXXX ");
543                 self.owner.gravity = 0;
544                 self.nextthink = self.owner.triggergravity = 0;
545                 remove(self);
546         }
547 }
548
549 void trigger_gravity_touch()
550 {
551         if(sv_gravity != 800)
552                 return;
553         // only do the EXACTTRIGGER_TOUCH checks when really needed (saves some cpu)
554         if (other.triggergravitytime < time)
555         {
556                 EXACTTRIGGER_TOUCH;
557                 other.triggergravitytime = time + 1;
558
559                 if(!other.triggergravity)
560                 {
561                         other.triggergravity = 1;
562                         other.trigger_gravity_check = spawn();
563                         other.trigger_gravity_check.owner = other;
564                         other.trigger_gravity_check.think = trigger_gravity_check_think;
565                         other.trigger_gravity_check.nextthink = time;
566                 }
567
568                 if (other.gravity != self.gravity)
569                 {
570                         other.gravity = self.gravity;
571                         if(self.noise != "")
572                                 sound (other, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NORM);
573                 }
574         }
575 };
576
577 void spawnfunc_trigger_gravity()
578 {
579         if(!self.gravity)
580                 return;
581         EXACTTRIGGER_INIT;
582         self.touch = trigger_gravity_touch;
583         if(self.noise != "")
584                 precache_sound(self.noise);
585 };
586
587 // TODO add a way to do looped sounds with sound(); then complete this entity
588 .float volume, atten;
589 void target_speaker_use() {sound(self, CHAN_TRIGGER, self.noise, VOL_BASE * self.volume, self.atten);}
590
591 void spawnfunc_target_speaker()
592 {
593         if(self.noise)
594                 precache_sound (self.noise);
595         IFTARGETED
596         {
597                 if(!self.atten)
598                         self.atten = ATTN_NORM;
599                 else if(self.atten < 0)
600                         self.atten = 0;
601                 if(!self.volume)
602                         self.volume = 1;
603                 self.use = target_speaker_use;
604         }
605         else
606         {
607                 if(!self.atten)
608                         self.atten = ATTN_STATIC;
609                 else if(self.atten < 0)
610                         self.atten = 0;
611                 if(!self.volume)
612                         self.volume = 1;
613                 ambientsound (self.origin, self.noise, VOL_BASE * self.volume, self.atten);
614         }
615 };
616
617
618 void spawnfunc_func_stardust() {
619         self.effects = EF_STARDUST;
620 }
621
622 .string bgmscript;
623 .float bgmscriptattack;
624 .float bgmscriptdecay;
625 .float bgmscriptsustain;
626 .float bgmscriptrelease;
627 float pointparticles_SendEntity(entity to, float fl)
628 {
629         WriteByte(MSG_ENTITY, ENT_CLIENT_POINTPARTICLES);
630
631         // optional features to save space
632         fl = fl & 0x0F;
633         if(self.spawnflags & 2)
634                 fl |= 0x10; // absolute count on toggle-on
635         if(self.movedir != '0 0 0' || self.velocity != '0 0 0')
636                 fl |= 0x20; // 4 bytes - saves CPU
637         if(self.waterlevel || self.count != 1)
638                 fl |= 0x40; // 4 bytes - obscure features almost never used
639         if(self.mins != '0 0 0' || self.maxs != '0 0 0')
640                 fl |= 0x80; // 14 bytes - saves lots of space
641
642         WriteByte(MSG_ENTITY, fl);
643         if(fl & 2)
644         {
645                 if(self.state)
646                         WriteCoord(MSG_ENTITY, self.impulse);
647                 else
648                         WriteCoord(MSG_ENTITY, 0); // off
649         }
650         if(fl & 4)
651         {
652                 WriteCoord(MSG_ENTITY, self.origin_x);
653                 WriteCoord(MSG_ENTITY, self.origin_y);
654                 WriteCoord(MSG_ENTITY, self.origin_z);
655         }
656         if(fl & 1)
657         {
658                 if(self.model != "null")
659                 {
660                         WriteShort(MSG_ENTITY, self.modelindex);
661                         if(fl & 0x80)
662                         {
663                                 WriteCoord(MSG_ENTITY, self.mins_x);
664                                 WriteCoord(MSG_ENTITY, self.mins_y);
665                                 WriteCoord(MSG_ENTITY, self.mins_z);
666                                 WriteCoord(MSG_ENTITY, self.maxs_x);
667                                 WriteCoord(MSG_ENTITY, self.maxs_y);
668                                 WriteCoord(MSG_ENTITY, self.maxs_z);
669                         }
670                 }
671                 else
672                 {
673                         WriteShort(MSG_ENTITY, 0);
674                         if(fl & 0x80)
675                         {
676                                 WriteCoord(MSG_ENTITY, self.maxs_x);
677                                 WriteCoord(MSG_ENTITY, self.maxs_y);
678                                 WriteCoord(MSG_ENTITY, self.maxs_z);
679                         }
680                 }
681                 WriteShort(MSG_ENTITY, self.cnt);
682                 if(fl & 0x20)
683                 {
684                         WriteShort(MSG_ENTITY, compressShortVector(self.velocity));
685                         WriteShort(MSG_ENTITY, compressShortVector(self.movedir));
686                 }
687                 if(fl & 0x40)
688                 {
689                         WriteShort(MSG_ENTITY, self.waterlevel * 16.0);
690                         WriteByte(MSG_ENTITY, self.count * 16.0);
691                 }
692                 WriteString(MSG_ENTITY, self.noise);
693                 if(self.noise != "")
694                 {
695                         WriteByte(MSG_ENTITY, floor(self.atten * 64));
696                         WriteByte(MSG_ENTITY, floor(self.volume * 255));
697                 }
698                 WriteString(MSG_ENTITY, self.bgmscript);
699                 if(self.bgmscript != "")
700                 {
701                         WriteByte(MSG_ENTITY, floor(self.bgmscriptattack * 64));
702                         WriteByte(MSG_ENTITY, floor(self.bgmscriptdecay * 64));
703                         WriteByte(MSG_ENTITY, floor(self.bgmscriptsustain * 255));
704                         WriteByte(MSG_ENTITY, floor(self.bgmscriptrelease * 64));
705                 }
706         }
707         return 1;
708 }
709
710 void pointparticles_use()
711 {
712         self.state = !self.state;
713         self.SendFlags |= 2;
714 }
715
716 void pointparticles_think()
717 {
718         if(self.origin != self.oldorigin)
719         {
720                 self.SendFlags |= 4;
721                 self.oldorigin = self.origin;
722         }
723         self.nextthink = time;
724 }
725
726 void pointparticles_reset()
727 {
728         if(self.spawnflags & 1)
729                 self.state = 1;
730         else
731                 self.state = 0;
732 }
733
734 void spawnfunc_func_pointparticles()
735 {
736         if(self.model != "")
737                 setmodel(self, self.model);
738         if(self.noise != "")
739                 precache_sound (self.noise);
740         
741         if(!self.bgmscriptsustain)
742                 self.bgmscriptsustain = 1;
743         else if(self.bgmscriptsustain < 0)
744                 self.bgmscriptsustain = 0;
745
746         if(!self.atten)
747                 self.atten = ATTN_NORM;
748         else if(self.atten < 0)
749                 self.atten = 0;
750         if(!self.volume)
751                 self.volume = 1;
752         if(!self.count)
753                 self.count = 1;
754         if(!self.impulse)
755                 self.impulse = 1;
756
757         if(!self.modelindex)
758         {
759                 setorigin(self, self.origin + self.mins);
760                 setsize(self, '0 0 0', self.maxs - self.mins);
761         }
762         if(!self.cnt)
763                 self.cnt = particleeffectnum(self.mdl);
764
765         Net_LinkEntity(self, FALSE, 0, pointparticles_SendEntity);
766
767         IFTARGETED
768         {
769                 self.use = pointparticles_use;
770                 self.reset = pointparticles_reset;
771                 self.reset();
772         }
773         else
774                 self.state = 1;
775         self.think = pointparticles_think;
776         self.nextthink = time;
777 }
778
779 void spawnfunc_func_sparks()
780 {
781         // self.cnt is the amount of sparks that one burst will spawn
782         if(self.cnt < 1) {
783                 self.cnt = 25.0; // nice default value
784         }
785
786         // self.wait is the probability that a sparkthink will spawn a spark shower
787         // range: 0 - 1, but 0 makes little sense, so...
788         if(self.wait < 0.05) {
789                 self.wait = 0.25; // nice default value
790         }
791
792         self.count = self.cnt;
793         self.mins = '0 0 0';
794         self.maxs = '0 0 0';
795         self.velocity = '0 0 -1';
796         self.mdl = "TE_SPARK";
797         self.impulse = 10 * self.wait; // by default 2.5/sec
798         self.wait = 0;
799         self.cnt = 0; // use mdl
800
801         spawnfunc_func_pointparticles();
802 }
803
804 float rainsnow_SendEntity(entity to, float sf)
805 {
806         WriteByte(MSG_ENTITY, ENT_CLIENT_RAINSNOW);
807         WriteByte(MSG_ENTITY, self.state);
808         WriteCoord(MSG_ENTITY, self.origin_x + self.mins_x);
809         WriteCoord(MSG_ENTITY, self.origin_y + self.mins_y);
810         WriteCoord(MSG_ENTITY, self.origin_z + self.mins_z);
811         WriteCoord(MSG_ENTITY, self.maxs_x - self.mins_x);
812         WriteCoord(MSG_ENTITY, self.maxs_y - self.mins_y);
813         WriteCoord(MSG_ENTITY, self.maxs_z - self.mins_z);
814         WriteShort(MSG_ENTITY, compressShortVector(self.dest));
815         WriteShort(MSG_ENTITY, self.count);
816         WriteByte(MSG_ENTITY, self.cnt);
817         return 1;
818 };
819
820 /*QUAKED spawnfunc_func_rain (0 .5 .8) ?
821 This is an invisible area like a trigger, which rain falls inside of.
822
823 Keys:
824 "velocity"
825  falling direction (should be something like '0 0 -700', use the X and Y velocity for wind)
826 "cnt"
827  sets color of rain (default 12 - white)
828 "count"
829  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
830 */
831 void spawnfunc_func_rain()
832 {
833         self.dest = self.velocity;
834         self.velocity = '0 0 0';
835         if (!self.dest)
836                 self.dest = '0 0 -700';
837         self.angles = '0 0 0';
838         self.movetype = MOVETYPE_NONE;
839         self.solid = SOLID_NOT;
840         SetBrushEntityModel();
841         if (!self.cnt)
842                 self.cnt = 12;
843         if (!self.count)
844                 self.count = 2000;
845         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
846         if (self.count < 1)
847                 self.count = 1;
848         if(self.count > 65535)
849                 self.count = 65535;
850
851         self.state = 1; // 1 is rain, 0 is snow
852         self.Version = 1;
853
854         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
855 };
856
857
858 /*QUAKED spawnfunc_func_snow (0 .5 .8) ?
859 This is an invisible area like a trigger, which snow falls inside of.
860
861 Keys:
862 "velocity"
863  falling direction (should be something like '0 0 -300', use the X and Y velocity for wind)
864 "cnt"
865  sets color of rain (default 12 - white)
866 "count"
867  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
868 */
869 void spawnfunc_func_snow()
870 {
871         self.dest = self.velocity;
872         self.velocity = '0 0 0';
873         if (!self.dest)
874                 self.dest = '0 0 -300';
875         self.angles = '0 0 0';
876         self.movetype = MOVETYPE_NONE;
877         self.solid = SOLID_NOT;
878         SetBrushEntityModel();
879         if (!self.cnt)
880                 self.cnt = 12;
881         if (!self.count)
882                 self.count = 2000;
883         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
884         if (self.count < 1)
885                 self.count = 1;
886         if(self.count > 65535)
887                 self.count = 65535;
888
889         self.state = 0; // 1 is rain, 0 is snow
890         self.Version = 1;
891
892         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
893 };
894
895
896 void FireRailgunBullet (vector start, vector end, float bdamage, float bforce, float mindist, float maxdist, float halflifedist, float forcehalflifedist, float deathtype);
897
898 .float modelscale;
899 void misc_laser_aim()
900 {
901         vector a;
902         if(self.enemy)
903         {
904                 if(self.spawnflags & 2)
905                 {
906                         if(self.enemy.origin != self.mangle)
907                         {
908                                 self.mangle = self.enemy.origin;
909                                 self.SendFlags |= 2;
910                         }
911                 }
912                 else
913                 {
914                         a = vectoangles(self.enemy.origin - self.origin);
915                         a_x = -a_x;
916                         if(a != self.mangle)
917                         {
918                                 self.mangle = a;
919                                 self.SendFlags |= 2;
920                         }
921                 }
922         }
923         else
924         {
925                 if(self.angles != self.mangle)
926                 {
927                         self.mangle = self.angles;
928                         self.SendFlags |= 2;
929                 }
930         }
931         if(self.origin != self.oldorigin)
932         {
933                 self.SendFlags |= 1;
934                 self.oldorigin = self.origin;
935         }
936 }
937
938 void misc_laser_init()
939 {
940         if(self.target != "")
941                 self.enemy = find(world, targetname, self.target);
942 }
943
944 .entity pusher;
945 void misc_laser_think()
946 {
947         vector o;
948         entity oldself;
949
950         self.nextthink = time;
951
952         if(!self.state)
953                 return;
954
955         misc_laser_aim();
956
957         if(self.enemy)
958         {
959                 o = self.enemy.origin;
960                 if not(self.spawnflags & 2)
961                         o = self.origin + normalize(o - self.origin) * 32768;
962         }
963         else
964         {
965                 makevectors(self.mangle);
966                 o = self.origin + v_forward * 32768;
967         }
968
969         if(self.dmg)
970         {
971                 if(self.dmg < 0)
972                         FireRailgunBullet(self.origin, o, 100000, 0, 0, 0, 0, 0, DEATH_HURTTRIGGER);
973                 else
974                         FireRailgunBullet(self.origin, o, self.dmg * frametime, 0, 0, 0, 0, 0, DEATH_HURTTRIGGER);
975         }
976
977         if(self.enemy.target != "") // DETECTOR laser
978         {
979                 traceline(self.origin, o, MOVE_NORMAL, self);
980                 if(trace_ent.iscreature)
981                 {
982                         self.pusher = trace_ent;
983                         if(!self.count)
984                         {
985                                 self.count = 1;
986
987                                 oldself = self;
988                                 self = self.enemy;
989                                 activator = self.pusher;
990                                 SUB_UseTargets();
991                                 self = oldself;
992                         }
993                 }
994                 else
995                 {
996                         if(self.count)
997                         {
998                                 self.count = 0;
999
1000                                 oldself = self;
1001                                 self = self.enemy;
1002                                 activator = self.pusher;
1003                                 SUB_UseTargets();
1004                                 self = oldself;
1005                         }
1006                 }
1007         }
1008 }
1009
1010 float laser_SendEntity(entity to, float fl)
1011 {
1012         WriteByte(MSG_ENTITY, ENT_CLIENT_LASER);
1013         fl = fl - (fl & 0xE0); // use that bit to indicate finite length laser
1014         if(self.spawnflags & 2)
1015                 fl |= 0x80;
1016         if(self.alpha)
1017                 fl |= 0x40;
1018         if(self.scale != 1 || self.modelscale != 1)
1019                 fl |= 0x20;
1020         WriteByte(MSG_ENTITY, fl);
1021         if(fl & 1)
1022         {
1023                 WriteCoord(MSG_ENTITY, self.origin_x);
1024                 WriteCoord(MSG_ENTITY, self.origin_y);
1025                 WriteCoord(MSG_ENTITY, self.origin_z);
1026         }
1027         if(fl & 8)
1028         {
1029                 WriteByte(MSG_ENTITY, self.colormod_x * 255.0);
1030                 WriteByte(MSG_ENTITY, self.colormod_y * 255.0);
1031                 WriteByte(MSG_ENTITY, self.colormod_z * 255.0);
1032                 if(fl & 0x40)
1033                         WriteByte(MSG_ENTITY, self.alpha * 255.0);
1034                 if(fl & 0x20)
1035                 {
1036                         WriteByte(MSG_ENTITY, bound(0, self.scale * 16.0, 255));
1037                         WriteByte(MSG_ENTITY, bound(0, self.modelscale * 16.0, 255));
1038                 }
1039                 WriteShort(MSG_ENTITY, self.cnt + 1);
1040         }
1041         if(fl & 2)
1042         {
1043                 if(fl & 0x80)
1044                 {
1045                         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
1046                         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
1047                         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
1048                 }
1049                 else
1050                 {
1051                         WriteAngle(MSG_ENTITY, self.mangle_x);
1052                         WriteAngle(MSG_ENTITY, self.mangle_y);
1053                 }
1054         }
1055         if(fl & 4)
1056                 WriteByte(MSG_ENTITY, self.state);
1057         return 1;
1058 }
1059
1060 /*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
1061 Any object touching the beam will be hurt
1062 Keys:
1063 "target"
1064  spawnfunc_target_position where the laser ends
1065 "mdl"
1066  name of beam end effect to use
1067 "colormod"
1068  color of the beam (default: red)
1069 "dmg"
1070  damage per second (-1 for a laser that kills immediately)
1071 */
1072 void laser_use()
1073 {
1074         self.state = !self.state;
1075         self.SendFlags |= 4;
1076         misc_laser_aim();
1077 }
1078
1079 void laser_reset()
1080 {
1081         if(self.spawnflags & 1)
1082                 self.state = 1;
1083         else
1084                 self.state = 0;
1085 }
1086
1087 void spawnfunc_misc_laser()
1088 {
1089         if(self.mdl)
1090         {
1091                 if(self.mdl == "none")
1092                         self.cnt = -1;
1093                 else
1094                 {
1095                         self.cnt = particleeffectnum(self.mdl);
1096                         if(self.cnt < 0)
1097                                 if(self.dmg)
1098                                         self.cnt = particleeffectnum("laser_deadly");
1099                 }
1100         }
1101         else if(!self.cnt)
1102         {
1103                 if(self.dmg)
1104                         self.cnt = particleeffectnum("laser_deadly");
1105                 else
1106                         self.cnt = -1;
1107         }
1108         if(self.cnt < 0)
1109                 self.cnt = -1;
1110
1111         if(self.colormod == '0 0 0')
1112                 if(!self.alpha)
1113                         self.colormod = '1 0 0';
1114         if(!self.message)
1115                 self.message = "saw the light";
1116         if (!self.message2)
1117                 self.message2 = "was pushed into a laser by";
1118         if(!self.scale)
1119                 self.scale = 1;
1120         if(!self.modelscale)
1121                 self.modelscale = 1;
1122         self.think = misc_laser_think;
1123         self.nextthink = time;
1124         InitializeEntity(self, misc_laser_init, INITPRIO_FINDTARGET);
1125
1126         self.mangle = self.angles;
1127
1128         Net_LinkEntity(self, FALSE, 0, laser_SendEntity);
1129
1130         IFTARGETED
1131         {
1132                 self.reset = laser_reset;
1133                 laser_reset();
1134                 self.use = laser_use;
1135         }
1136         else
1137                 self.state = 1;
1138 }
1139
1140 // tZorks trigger impulse / gravity
1141 .float radius;
1142 .float falloff;
1143 .float strength;
1144 .float lastpushtime;
1145
1146 // targeted (directional) mode
1147 void trigger_impulse_touch1()
1148 {
1149         entity targ;
1150     float pushdeltatime;
1151     float str;
1152
1153         // FIXME: Better checking for what to push and not.
1154         if not(other.iscreature)
1155         if (other.classname != "corpse")
1156         if (other.classname != "body")
1157         if (other.classname != "gib")
1158         if (other.classname != "missile")
1159         if (other.classname != "rocket")
1160         if (other.classname != "casing")
1161         if (other.classname != "grenade")
1162         if (other.classname != "plasma")
1163         if (other.classname != "plasma_prim")
1164         if (other.classname != "plasma_chain")
1165         if (other.classname != "droppedweapon")
1166         if (other.classname != "nexball_basketball")
1167         if (other.classname != "nexball_football")
1168                 return;
1169
1170         if (other.deadflag && other.iscreature)
1171                 return;
1172
1173         EXACTTRIGGER_TOUCH;
1174
1175     targ = find(world, targetname, self.target);
1176     if(!targ)
1177     {
1178         objerror("trigger_force without a (valid) .target!\n");
1179         remove(self);
1180         return;
1181     }
1182
1183     if(self.falloff == 1)
1184         str = (str / self.radius) * self.strength;
1185     else if(self.falloff == 2)
1186         str = (1 - (str / self.radius)) * self.strength;
1187     else
1188         str = self.strength;
1189
1190     pushdeltatime = time - other.lastpushtime;
1191     if (pushdeltatime > 0.15) pushdeltatime = 0;
1192     other.lastpushtime = time;
1193     if(!pushdeltatime) return;
1194
1195     other.velocity = other.velocity + normalize(targ.origin - self.origin) * str * pushdeltatime;
1196         other.flags &~= FL_ONGROUND;
1197 }
1198
1199 // Directionless (accelerator/decelerator) mode
1200 void trigger_impulse_touch2()
1201 {
1202     float pushdeltatime;
1203
1204         // FIXME: Better checking for what to push and not.
1205         if not(other.iscreature)
1206         if (other.classname != "corpse")
1207         if (other.classname != "body")
1208         if (other.classname != "gib")
1209         if (other.classname != "missile")
1210         if (other.classname != "rocket")
1211         if (other.classname != "casing")
1212         if (other.classname != "grenade")
1213         if (other.classname != "plasma")
1214         if (other.classname != "plasma_prim")
1215         if (other.classname != "plasma_chain")
1216         if (other.classname != "droppedweapon")
1217         if (other.classname != "nexball_basketball")
1218         if (other.classname != "nexball_football")
1219                 return;
1220
1221         if (other.deadflag && other.iscreature)
1222                 return;
1223
1224         EXACTTRIGGER_TOUCH;
1225
1226     pushdeltatime = time - other.lastpushtime;
1227     if (pushdeltatime > 0.15) pushdeltatime = 0;
1228     other.lastpushtime = time;
1229     if(!pushdeltatime) return;
1230
1231     // div0: ticrate independent, 1 = identity (not 20)
1232     other.velocity = other.velocity * pow(self.strength, pushdeltatime);
1233 }
1234
1235 // Spherical (gravity/repulsor) mode
1236 void trigger_impulse_touch3()
1237 {
1238     float pushdeltatime;
1239     float str;
1240
1241         // FIXME: Better checking for what to push and not.
1242         if not(other.iscreature)
1243         if (other.classname != "corpse")
1244         if (other.classname != "body")
1245         if (other.classname != "gib")
1246         if (other.classname != "missile")
1247         if (other.classname != "rocket")
1248         if (other.classname != "casing")
1249         if (other.classname != "grenade")
1250         if (other.classname != "plasma")
1251         if (other.classname != "plasma_prim")
1252         if (other.classname != "plasma_chain")
1253         if (other.classname != "droppedweapon")
1254         if (other.classname != "nexball_basketball")
1255         if (other.classname != "nexball_football")
1256                 return;
1257
1258         if (other.deadflag && other.iscreature)
1259                 return;
1260
1261         EXACTTRIGGER_TOUCH;
1262
1263     pushdeltatime = time - other.lastpushtime;
1264     if (pushdeltatime > 0.15) pushdeltatime = 0;
1265     other.lastpushtime = time;
1266     if(!pushdeltatime) return;
1267
1268     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1269
1270         str = min(self.radius, vlen(self.origin - other.origin));
1271
1272     if(self.falloff == 1)
1273         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1274     else if(self.falloff == 2)
1275         str = (str / self.radius) * self.strength; // 0 in the inside
1276     else
1277         str = self.strength;
1278
1279     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1280 }
1281
1282 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1283 -------- KEYS --------
1284 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1285          If not, this trigger acts like a damper/accelerator field.
1286
1287 strength : This is how mutch force to add in the direction of .target each second
1288            when .target is set. If not, this is hoe mutch to slow down/accelerate
1289            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1290
1291 radius   : If set, act as a spherical device rather then a liniar one.
1292
1293 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1294
1295 -------- NOTES --------
1296 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1297 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1298 */
1299
1300 void spawnfunc_trigger_impulse()
1301 {
1302         EXACTTRIGGER_INIT;
1303     if(self.radius)
1304     {
1305         if(!self.strength) self.strength = 2000 * cvar("g_triggerimpulse_radial_multiplier");
1306         setorigin(self, self.origin);
1307         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1308         self.touch = trigger_impulse_touch3;
1309     }
1310     else
1311     {
1312         if(self.target)
1313         {
1314             if(!self.strength) self.strength = 950 * cvar("g_triggerimpulse_directional_multiplier");
1315             self.touch = trigger_impulse_touch1;
1316         }
1317         else
1318         {
1319             if(!self.strength) self.strength = 0.9;
1320                         self.strength = pow(self.strength, cvar("g_triggerimpulse_accel_power")) * cvar("g_triggerimpulse_accel_multiplier");
1321             self.touch = trigger_impulse_touch2;
1322         }
1323     }
1324 }
1325
1326 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1327 "Flip-flop" trigger gate... lets only every second trigger event through
1328 */
1329 void flipflop_use()
1330 {
1331         self.state = !self.state;
1332         if(self.state)
1333                 SUB_UseTargets();
1334 }
1335
1336 void spawnfunc_trigger_flipflop()
1337 {
1338         if(self.spawnflags & 1)
1339                 self.state = 1;
1340         self.use = flipflop_use;
1341         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1342 }
1343
1344 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1345 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1346 */
1347 void monoflop_use()
1348 {
1349         self.nextthink = time + self.wait;
1350         self.enemy = activator;
1351         if(self.state)
1352                 return;
1353         self.state = 1;
1354         SUB_UseTargets();
1355 }
1356 void monoflop_fixed_use()
1357 {
1358         if(self.state)
1359                 return;
1360         self.nextthink = time + self.wait;
1361         self.state = 1;
1362         self.enemy = activator;
1363         SUB_UseTargets();
1364 }
1365
1366 void monoflop_think()
1367 {
1368         self.state = 0;
1369         activator = self.enemy;
1370         SUB_UseTargets();
1371 }
1372
1373 void monoflop_reset()
1374 {
1375         self.state = 0;
1376         self.nextthink = 0;
1377 }
1378
1379 void spawnfunc_trigger_monoflop()
1380 {
1381         if(!self.wait)
1382                 self.wait = 1;
1383         if(self.spawnflags & 1)
1384                 self.use = monoflop_fixed_use;
1385         else
1386                 self.use = monoflop_use;
1387         self.think = monoflop_think;
1388         self.state = 0;
1389         self.reset = monoflop_reset;
1390 }
1391
1392 void multivibrator_send()
1393 {
1394         float newstate;
1395         float cyclestart;
1396
1397         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1398
1399         newstate = (time < cyclestart + self.wait);
1400
1401         activator = self;
1402         if(self.state != newstate)
1403                 SUB_UseTargets();
1404         self.state = newstate;
1405
1406         if(self.state)
1407                 self.nextthink = cyclestart + self.wait + 0.01;
1408         else
1409                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1410 }
1411
1412 void multivibrator_toggle()
1413 {
1414         if(self.nextthink == 0)
1415         {
1416                 multivibrator_send();
1417         }
1418         else
1419         {
1420                 if(self.state)
1421                 {
1422                         SUB_UseTargets();
1423                         self.state = 0;
1424                 }
1425                 self.nextthink = 0;
1426         }
1427 }
1428
1429 void multivibrator_reset()
1430 {
1431         if(!(self.spawnflags & 1))
1432                 self.nextthink = 0; // wait for a trigger event
1433         else
1434                 self.nextthink = max(1, time);
1435 }
1436
1437 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1438 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1439 -------- KEYS --------
1440 target: trigger all entities with this targetname when it goes off
1441 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1442 phase: offset of the timing
1443 wait: "on" cycle time (default: 1)
1444 respawntime: "off" cycle time (default: same as wait)
1445 -------- SPAWNFLAGS --------
1446 START_ON: assume it is already turned on (when targeted)
1447 */
1448 void spawnfunc_trigger_multivibrator()
1449 {
1450         if(!self.wait)
1451                 self.wait = 1;
1452         if(!self.respawntime)
1453                 self.respawntime = self.wait;
1454
1455         self.state = 0;
1456         self.use = multivibrator_toggle;
1457         self.think = multivibrator_send;
1458         self.nextthink = time;
1459
1460         IFTARGETED
1461                 multivibrator_reset();
1462 }
1463
1464
1465 void follow_init()
1466 {
1467         entity src, dst;
1468         src = world;
1469         dst = world;
1470         if(self.killtarget != "")
1471                 src = find(world, targetname, self.killtarget);
1472         if(self.target != "")
1473                 dst = find(world, targetname, self.target);
1474
1475         if(!src && !dst)
1476         {
1477                 objerror("follow: could not find target/killtarget");
1478                 return;
1479         }
1480
1481         if(self.jointtype)
1482         {
1483                 // already done :P entity must stay
1484                 self.aiment = src;
1485                 self.enemy = dst;
1486         }
1487         else if(!src || !dst)
1488         {
1489                 objerror("follow: could not find target/killtarget");
1490                 return;
1491         }
1492         else if(self.spawnflags & 1)
1493         {
1494                 // attach
1495                 if(self.spawnflags & 2)
1496                 {
1497                         setattachment(dst, src, self.message);
1498                 }
1499                 else
1500                 {
1501                         attach_sameorigin(dst, src, self.message);
1502                 }
1503
1504                 remove(self);
1505         }
1506         else
1507         {
1508                 if(self.spawnflags & 2)
1509                 {
1510                         dst.movetype = MOVETYPE_FOLLOW;
1511                         dst.aiment = src;
1512                         // dst.punchangle = '0 0 0'; // keep unchanged
1513                         dst.view_ofs = dst.origin;
1514                         dst.v_angle = dst.angles;
1515                 }
1516                 else
1517                 {
1518                         follow_sameorigin(dst, src);
1519                 }
1520
1521                 remove(self);
1522         }
1523 }
1524
1525 void spawnfunc_misc_follow()
1526 {
1527         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1528 }
1529
1530
1531
1532 void gamestart_use() {
1533         activator = self;
1534         SUB_UseTargets();
1535         remove(self);
1536 }
1537
1538 void spawnfunc_trigger_gamestart() {
1539         self.use = gamestart_use;
1540         self.reset2 = spawnfunc_trigger_gamestart;
1541
1542         if(self.wait)
1543         {
1544                 self.think = self.use;
1545                 self.nextthink = game_starttime + self.wait;
1546         }
1547         else
1548                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1549 }
1550
1551
1552
1553
1554 .entity voicescript; // attached voice script
1555 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1556 .float voicescript_nextthink; // time to play next voice
1557 .float voicescript_voiceend; // time when this voice ends
1558
1559 void target_voicescript_clear(entity pl)
1560 {
1561         pl.voicescript = world;
1562 }
1563
1564 void target_voicescript_use()
1565 {
1566         if(activator.voicescript != self)
1567         {
1568                 activator.voicescript = self;
1569                 activator.voicescript_index = 0;
1570                 activator.voicescript_nextthink = time + self.delay;
1571         }
1572 }
1573
1574 void target_voicescript_next(entity pl)
1575 {
1576         entity vs;
1577         float i, n, dt;
1578
1579         vs = pl.voicescript;
1580         if(!vs)
1581                 return;
1582         if(vs.message == "")
1583                 return;
1584         if(pl.classname != "player")
1585                 return;
1586         if(gameover)
1587                 return;
1588
1589         if(time >= pl.voicescript_voiceend)
1590         {
1591                 if(time >= pl.voicescript_nextthink)
1592                 {
1593                         // get the next voice...
1594                         n = tokenize_console(vs.message);
1595
1596                         if(pl.voicescript_index < vs.cnt)
1597                                 i = pl.voicescript_index * 2;
1598                         else if(n > vs.cnt * 2)
1599                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1600                         else
1601                                 i = -1;
1602
1603                         if(i >= 0)
1604                         {
1605                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1606                                 dt = stof(argv(i + 1));
1607                                 if(dt >= 0)
1608                                 {
1609                                         pl.voicescript_voiceend = time + dt;
1610                                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1611                                 }
1612                                 else
1613                                 {
1614                                         pl.voicescript_voiceend = time - dt;
1615                                         pl.voicescript_nextthink = pl.voicescript_voiceend;
1616                                 }
1617
1618                                 pl.voicescript_index += 1;
1619                         }
1620                         else
1621                         {
1622                                 pl.voicescript = world; // stop trying then
1623                         }
1624                 }
1625         }
1626 }
1627
1628 void spawnfunc_target_voicescript()
1629 {
1630         // netname: directory of the sound files
1631         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1632         //          foo1 4.1 foo2 4.0 foo3 -3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1633         //          Here, a - in front of the duration means that no delay is to be
1634         //          added after this message
1635         // wait: average time between messages
1636         // delay: initial delay before the first message
1637         
1638         float i, n;
1639         self.use = target_voicescript_use;
1640
1641         n = tokenize_console(self.message);
1642         self.cnt = n / 2;
1643         for(i = 0; i+1 < n; i += 2)
1644         {
1645                 if(argv(i) == "*")
1646                 {
1647                         self.cnt = i / 2;
1648                         ++i;
1649                 }
1650                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1651         }
1652 }
1653
1654
1655
1656 void trigger_relay_teamcheck_use()
1657 {
1658         if(activator.team)
1659         {
1660                 if(self.spawnflags & 2)
1661                 {
1662                         if(activator.team != self.team)
1663                                 SUB_UseTargets();
1664                 }
1665                 else
1666                 {
1667                         if(activator.team == self.team)
1668                                 SUB_UseTargets();
1669                 }
1670         }
1671         else
1672         {
1673                 if(self.spawnflags & 1)
1674                         SUB_UseTargets();
1675         }
1676 }
1677
1678 void trigger_relay_teamcheck_reset()
1679 {
1680         self.team = self.team_saved;
1681 }
1682
1683 void spawnfunc_trigger_relay_teamcheck()
1684 {
1685         self.team_saved = self.team;
1686         self.use = trigger_relay_teamcheck_use;
1687         self.reset = trigger_relay_teamcheck_reset;
1688 }
1689
1690
1691
1692 void trigger_disablerelay_use()
1693 {
1694         entity e;
1695
1696         float a, b;
1697         a = b = 0;
1698
1699         for(e = world; (e = find(e, targetname, self.target)); )
1700         {
1701                 if(e.use == SUB_UseTargets)
1702                 {
1703                         e.use = SUB_DontUseTargets;
1704                         ++a;
1705                 }
1706                 else if(e.use == SUB_DontUseTargets)
1707                 {
1708                         e.use = SUB_UseTargets;
1709                         ++b;
1710                 }
1711         }
1712
1713         if((!a) == (!b))
1714                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1715 }
1716
1717 void spawnfunc_trigger_disablerelay()
1718 {
1719         self.use = trigger_disablerelay_use;
1720 }
1721
1722 float magicear_matched;
1723 string trigger_magicear_processmessage(entity ear, entity source, float teamsay, entity privatesay, string msgin)
1724 {
1725         float domatch, dotrigger, matchstart, l;
1726         string s, msg;
1727         entity oldself;
1728
1729         magicear_matched = FALSE;
1730
1731         dotrigger = ((self.classname == "player") && (self.deadflag == DEAD_NO) && ((ear.radius == 0) || (vlen(source.origin - ear.origin) <= ear.radius)));
1732         domatch = ((ear.spawnflags & 32) || dotrigger);
1733         if not(domatch)
1734                 return msgin;
1735
1736         if(privatesay)
1737         {
1738                 if(ear.spawnflags & 4)
1739                         return msgin;
1740         }
1741         else
1742         {
1743                 if(!teamsay)
1744                         if(ear.spawnflags & 1)
1745                                 return msgin;
1746                 if(teamsay > 0)
1747                         if(ear.spawnflags & 2)
1748                                 return msgin;
1749                 if(teamsay < 0)
1750                         if(ear.spawnflags & 8)
1751                                 return msgin;
1752         }
1753         
1754         matchstart = -1;
1755         l = strlen(ear.message);
1756
1757         if(self.spawnflags & 128)
1758                 msg = msgin;
1759         else
1760                 msg = strdecolorize(msgin);
1761
1762         if(substring(ear.message, 0, 1) == "*")
1763         {
1764                 if(substring(ear.message, -1, 1) == "*")
1765                 {
1766                         // two wildcards
1767                         // as we need multi-replacement here...
1768                         s = substring(ear.message, 1, -2);
1769                         l -= 2;
1770                         if(strstrofs(msg, s, 0) >= 0)
1771                                 matchstart = -2; // we use strreplace on s
1772                 }
1773                 else
1774                 {
1775                         // match at start
1776                         s = substring(ear.message, 1, -1);
1777                         l -= 1;
1778                         if(substring(msg, -l, l) == s)
1779                                 matchstart = strlen(msg) - l;
1780                 }
1781         }
1782         else
1783         {
1784                 if(substring(ear.message, -1, 1) == "*")
1785                 {
1786                         // match at end
1787                         s = substring(ear.message, 0, -2);
1788                         l -= 1;
1789                         if(substring(msg, 0, l) == s)
1790                                 matchstart = 0;
1791                 }
1792                 else
1793                 {
1794                         // full match
1795                         s = ear.message;
1796                         if(msg == ear.message)
1797                                 matchstart = 0;
1798                 }
1799         }
1800
1801         if(matchstart == -1) // no match
1802                 return msgin;
1803
1804         magicear_matched = TRUE;
1805
1806         if(dotrigger)
1807         {
1808                 oldself = activator = self;
1809                 self = ear;
1810                 SUB_UseTargets();
1811                 self = oldself;
1812         }
1813
1814         if(ear.spawnflags & 16)
1815         {
1816                 return ear.netname;
1817         }
1818         else if(ear.netname != "")
1819         {
1820                 if(matchstart < 0)
1821                         return strreplace(s, ear.netname, msg);
1822                 else
1823                         return strcat(
1824                                 substring(msg, 0, matchstart),
1825                                 ear.netname,
1826                                 substring(msg, matchstart + l, -1)
1827                         );
1828         }
1829         else
1830                 return msgin;
1831 }
1832
1833 entity magicears;
1834 string trigger_magicear_processmessage_forallears(entity source, float teamsay, entity privatesay, string msgin)
1835 {
1836         entity ear;
1837         string msgout;
1838         for(ear = magicears; ear; ear = ear.enemy)
1839         {
1840                 msgout = trigger_magicear_processmessage(ear, source, teamsay, privatesay, msgin);
1841                 if not(ear.spawnflags & 64)
1842                         if(magicear_matched)
1843                                 return msgout;
1844                 msgin = msgout;
1845         }
1846         return msgin;
1847 }
1848
1849 void spawnfunc_trigger_magicear()
1850 {
1851         self.enemy = magicears;
1852         magicears = self;
1853
1854         // actually handled in "say" processing
1855         // spawnflags:
1856         //   1 = ignore say
1857         //   2 = ignore teamsay
1858         //   4 = ignore tell
1859         //   8 = ignore tell to unknown player
1860         //   16 = let netname replace the whole message (otherwise, netname is a word replacement if set)
1861         //   32 = perform the replacement even if outside the radius or dead
1862         //   64 = continue replacing/triggering even if this one matched
1863         // message: either
1864         //   *pattern*
1865         // or
1866         //   *pattern
1867         // or
1868         //   pattern*
1869         // or
1870         //   pattern
1871         // netname:
1872         //   if set, replacement for the matched text
1873         // radius:
1874         //   "hearing distance"
1875         // target:
1876         //   what to trigger
1877 }