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