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