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