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