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