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