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