]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_triggers.qc
Merge branch 'master' into Mario/notifications
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / g_triggers.qc
1 void SUB_DontUseTargets()
2 {
3 }
4
5
6 void() SUB_UseTargets;
7
8 void DelayThink()
9 {
10         activator = self.enemy;
11         SUB_UseTargets ();
12         remove(self);
13 }
14
15 /*
16 ==============================
17 SUB_UseTargets
18
19 the global "activator" should be set to the entity that initiated the firing.
20
21 If self.delay is set, a DelayedUse entity will be created that will actually
22 do the SUB_UseTargets after that many seconds have passed.
23
24 Centerprints any self.message to the activator.
25
26 Removes all entities with a targetname that match self.killtarget,
27 and removes them, so some events can remove other triggers.
28
29 Search for (string)targetname in all entities that
30 match (string)self.target and call their .use function
31
32 ==============================
33 */
34 void SUB_UseTargets()
35 {
36         entity t, stemp, otemp, act;
37         string s;
38         float i;
39
40 //
41 // check for a delay
42 //
43         if (self.delay)
44         {
45         // create a temp object to fire at a later time
46                 t = spawn();
47                 t.classname = "DelayedUse";
48                 t.nextthink = time + self.delay;
49                 t.think = DelayThink;
50                 t.enemy = activator;
51                 t.message = self.message;
52                 t.killtarget = self.killtarget;
53                 t.target = self.target;
54                 t.target2 = self.target2;
55                 t.target3 = self.target3;
56                 t.target4 = self.target4;
57                 return;
58         }
59
60
61 //
62 // print the message
63 //
64         if(IS_PLAYER(activator) && self.message != "")
65         if(IS_REAL_CLIENT(activator))
66         {
67                 centerprint(activator, self.message);
68                 if (self.noise == "")
69                         play2(activator, "misc/talk.wav");
70         }
71
72 //
73 // kill the killtagets
74 //
75         s = self.killtarget;
76         if (s != "")
77         {
78                 for(t = world; (t = find(t, targetname, s)); )
79                         remove(t);
80         }
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
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 .string bgmscript;
820 .float bgmscriptattack;
821 .float bgmscriptdecay;
822 .float bgmscriptsustain;
823 .float bgmscriptrelease;
824 float pointparticles_SendEntity(entity to, float fl)
825 {
826         WriteByte(MSG_ENTITY, ENT_CLIENT_POINTPARTICLES);
827
828         // optional features to save space
829         fl = fl & 0x0F;
830         if(self.spawnflags & 2)
831                 fl |= 0x10; // absolute count on toggle-on
832         if(self.movedir != '0 0 0' || self.velocity != '0 0 0')
833                 fl |= 0x20; // 4 bytes - saves CPU
834         if(self.waterlevel || self.count != 1)
835                 fl |= 0x40; // 4 bytes - obscure features almost never used
836         if(self.mins != '0 0 0' || self.maxs != '0 0 0')
837                 fl |= 0x80; // 14 bytes - saves lots of space
838
839         WriteByte(MSG_ENTITY, fl);
840         if(fl & 2)
841         {
842                 if(self.state)
843                         WriteCoord(MSG_ENTITY, self.impulse);
844                 else
845                         WriteCoord(MSG_ENTITY, 0); // off
846         }
847         if(fl & 4)
848         {
849                 WriteCoord(MSG_ENTITY, self.origin_x);
850                 WriteCoord(MSG_ENTITY, self.origin_y);
851                 WriteCoord(MSG_ENTITY, self.origin_z);
852         }
853         if(fl & 1)
854         {
855                 if(self.model != "null")
856                 {
857                         WriteShort(MSG_ENTITY, self.modelindex);
858                         if(fl & 0x80)
859                         {
860                                 WriteCoord(MSG_ENTITY, self.mins_x);
861                                 WriteCoord(MSG_ENTITY, self.mins_y);
862                                 WriteCoord(MSG_ENTITY, self.mins_z);
863                                 WriteCoord(MSG_ENTITY, self.maxs_x);
864                                 WriteCoord(MSG_ENTITY, self.maxs_y);
865                                 WriteCoord(MSG_ENTITY, self.maxs_z);
866                         }
867                 }
868                 else
869                 {
870                         WriteShort(MSG_ENTITY, 0);
871                         if(fl & 0x80)
872                         {
873                                 WriteCoord(MSG_ENTITY, self.maxs_x);
874                                 WriteCoord(MSG_ENTITY, self.maxs_y);
875                                 WriteCoord(MSG_ENTITY, self.maxs_z);
876                         }
877                 }
878                 WriteShort(MSG_ENTITY, self.cnt);
879                 if(fl & 0x20)
880                 {
881                         WriteShort(MSG_ENTITY, compressShortVector(self.velocity));
882                         WriteShort(MSG_ENTITY, compressShortVector(self.movedir));
883                 }
884                 if(fl & 0x40)
885                 {
886                         WriteShort(MSG_ENTITY, self.waterlevel * 16.0);
887                         WriteByte(MSG_ENTITY, self.count * 16.0);
888                 }
889                 WriteString(MSG_ENTITY, self.noise);
890                 if(self.noise != "")
891                 {
892                         WriteByte(MSG_ENTITY, floor(self.atten * 64));
893                         WriteByte(MSG_ENTITY, floor(self.volume * 255));
894                 }
895                 WriteString(MSG_ENTITY, self.bgmscript);
896                 if(self.bgmscript != "")
897                 {
898                         WriteByte(MSG_ENTITY, floor(self.bgmscriptattack * 64));
899                         WriteByte(MSG_ENTITY, floor(self.bgmscriptdecay * 64));
900                         WriteByte(MSG_ENTITY, floor(self.bgmscriptsustain * 255));
901                         WriteByte(MSG_ENTITY, floor(self.bgmscriptrelease * 64));
902                 }
903         }
904         return 1;
905 }
906
907 void pointparticles_use()
908 {
909         self.state = !self.state;
910         self.SendFlags |= 2;
911 }
912
913 void pointparticles_think()
914 {
915         if(self.origin != self.oldorigin)
916         {
917                 self.SendFlags |= 4;
918                 self.oldorigin = self.origin;
919         }
920         self.nextthink = time;
921 }
922
923 void pointparticles_reset()
924 {
925         if(self.spawnflags & 1)
926                 self.state = 1;
927         else
928                 self.state = 0;
929 }
930
931 void spawnfunc_func_pointparticles()
932 {
933         if(self.model != "")
934                 setmodel(self, self.model);
935         if(self.noise != "")
936                 precache_sound (self.noise);
937
938         if(!self.bgmscriptsustain)
939                 self.bgmscriptsustain = 1;
940         else if(self.bgmscriptsustain < 0)
941                 self.bgmscriptsustain = 0;
942
943         if(!self.atten)
944                 self.atten = ATTEN_NORM;
945         else if(self.atten < 0)
946                 self.atten = 0;
947         if(!self.volume)
948                 self.volume = 1;
949         if(!self.count)
950                 self.count = 1;
951         if(!self.impulse)
952                 self.impulse = 1;
953
954         if(!self.modelindex)
955         {
956                 setorigin(self, self.origin + self.mins);
957                 setsize(self, '0 0 0', self.maxs - self.mins);
958         }
959         if(!self.cnt)
960                 self.cnt = particleeffectnum(self.mdl);
961
962         Net_LinkEntity(self, (self.spawnflags & 4), 0, pointparticles_SendEntity);
963
964         IFTARGETED
965         {
966                 self.use = pointparticles_use;
967                 self.reset = pointparticles_reset;
968                 self.reset();
969         }
970         else
971                 self.state = 1;
972         self.think = pointparticles_think;
973         self.nextthink = time;
974 }
975
976 void spawnfunc_func_sparks()
977 {
978         // self.cnt is the amount of sparks that one burst will spawn
979         if(self.cnt < 1) {
980                 self.cnt = 25.0; // nice default value
981         }
982
983         // self.wait is the probability that a sparkthink will spawn a spark shower
984         // range: 0 - 1, but 0 makes little sense, so...
985         if(self.wait < 0.05) {
986                 self.wait = 0.25; // nice default value
987         }
988
989         self.count = self.cnt;
990         self.mins = '0 0 0';
991         self.maxs = '0 0 0';
992         self.velocity = '0 0 -1';
993         self.mdl = "TE_SPARK";
994         self.impulse = 10 * self.wait; // by default 2.5/sec
995         self.wait = 0;
996         self.cnt = 0; // use mdl
997
998         spawnfunc_func_pointparticles();
999 }
1000
1001 float rainsnow_SendEntity(entity to, float sf)
1002 {
1003         WriteByte(MSG_ENTITY, ENT_CLIENT_RAINSNOW);
1004         WriteByte(MSG_ENTITY, self.state);
1005         WriteCoord(MSG_ENTITY, self.origin_x + self.mins_x);
1006         WriteCoord(MSG_ENTITY, self.origin_y + self.mins_y);
1007         WriteCoord(MSG_ENTITY, self.origin_z + self.mins_z);
1008         WriteCoord(MSG_ENTITY, self.maxs_x - self.mins_x);
1009         WriteCoord(MSG_ENTITY, self.maxs_y - self.mins_y);
1010         WriteCoord(MSG_ENTITY, self.maxs_z - self.mins_z);
1011         WriteShort(MSG_ENTITY, compressShortVector(self.dest));
1012         WriteShort(MSG_ENTITY, self.count);
1013         WriteByte(MSG_ENTITY, self.cnt);
1014         return 1;
1015 }
1016
1017 /*QUAKED spawnfunc_func_rain (0 .5 .8) ?
1018 This is an invisible area like a trigger, which rain falls inside of.
1019
1020 Keys:
1021 "velocity"
1022  falling direction (should be something like '0 0 -700', use the X and Y velocity for wind)
1023 "cnt"
1024  sets color of rain (default 12 - white)
1025 "count"
1026  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
1027 */
1028 void spawnfunc_func_rain()
1029 {
1030         self.dest = self.velocity;
1031         self.velocity = '0 0 0';
1032         if (!self.dest)
1033                 self.dest = '0 0 -700';
1034         self.angles = '0 0 0';
1035         self.movetype = MOVETYPE_NONE;
1036         self.solid = SOLID_NOT;
1037         SetBrushEntityModel();
1038         if (!self.cnt)
1039                 self.cnt = 12;
1040         if (!self.count)
1041                 self.count = 2000;
1042         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
1043         if (self.count < 1)
1044                 self.count = 1;
1045         if(self.count > 65535)
1046                 self.count = 65535;
1047
1048         self.state = 1; // 1 is rain, 0 is snow
1049         self.Version = 1;
1050
1051         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
1052 }
1053
1054
1055 /*QUAKED spawnfunc_func_snow (0 .5 .8) ?
1056 This is an invisible area like a trigger, which snow falls inside of.
1057
1058 Keys:
1059 "velocity"
1060  falling direction (should be something like '0 0 -300', use the X and Y velocity for wind)
1061 "cnt"
1062  sets color of rain (default 12 - white)
1063 "count"
1064  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
1065 */
1066 void spawnfunc_func_snow()
1067 {
1068         self.dest = self.velocity;
1069         self.velocity = '0 0 0';
1070         if (!self.dest)
1071                 self.dest = '0 0 -300';
1072         self.angles = '0 0 0';
1073         self.movetype = MOVETYPE_NONE;
1074         self.solid = SOLID_NOT;
1075         SetBrushEntityModel();
1076         if (!self.cnt)
1077                 self.cnt = 12;
1078         if (!self.count)
1079                 self.count = 2000;
1080         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
1081         if (self.count < 1)
1082                 self.count = 1;
1083         if(self.count > 65535)
1084                 self.count = 65535;
1085
1086         self.state = 0; // 1 is rain, 0 is snow
1087         self.Version = 1;
1088
1089         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
1090 }
1091
1092 .float modelscale;
1093 void misc_laser_aim()
1094 {
1095         vector a;
1096         if(self.enemy)
1097         {
1098                 if(self.spawnflags & 2)
1099                 {
1100                         if(self.enemy.origin != self.mangle)
1101                         {
1102                                 self.mangle = self.enemy.origin;
1103                                 self.SendFlags |= 2;
1104                         }
1105                 }
1106                 else
1107                 {
1108                         a = vectoangles(self.enemy.origin - self.origin);
1109                         a_x = -a_x;
1110                         if(a != self.mangle)
1111                         {
1112                                 self.mangle = a;
1113                                 self.SendFlags |= 2;
1114                         }
1115                 }
1116         }
1117         else
1118         {
1119                 if(self.angles != self.mangle)
1120                 {
1121                         self.mangle = self.angles;
1122                         self.SendFlags |= 2;
1123                 }
1124         }
1125         if(self.origin != self.oldorigin)
1126         {
1127                 self.SendFlags |= 1;
1128                 self.oldorigin = self.origin;
1129         }
1130 }
1131
1132 void misc_laser_init()
1133 {
1134         if(self.target != "")
1135                 self.enemy = find(world, targetname, self.target);
1136 }
1137
1138 .entity pusher;
1139 void misc_laser_think()
1140 {
1141         vector o;
1142         entity oldself;
1143         entity hitent;
1144         vector hitloc;
1145
1146         self.nextthink = time;
1147
1148         if(!self.state)
1149                 return;
1150
1151         misc_laser_aim();
1152
1153         if(self.enemy)
1154         {
1155                 o = self.enemy.origin;
1156                 if (!(self.spawnflags & 2))
1157                         o = self.origin + normalize(o - self.origin) * 32768;
1158         }
1159         else
1160         {
1161                 makevectors(self.mangle);
1162                 o = self.origin + v_forward * 32768;
1163         }
1164
1165         if(self.dmg || self.enemy.target != "")
1166         {
1167                 traceline(self.origin, o, MOVE_NORMAL, self);
1168         }
1169         hitent = trace_ent;
1170         hitloc = trace_endpos;
1171
1172         if(self.enemy.target != "") // DETECTOR laser
1173         {
1174                 if(trace_ent.iscreature)
1175                 {
1176                         self.pusher = hitent;
1177                         if(!self.count)
1178                         {
1179                                 self.count = 1;
1180
1181                                 oldself = self;
1182                                 self = self.enemy;
1183                                 activator = self.pusher;
1184                                 SUB_UseTargets();
1185                                 self = oldself;
1186                         }
1187                 }
1188                 else
1189                 {
1190                         if(self.count)
1191                         {
1192                                 self.count = 0;
1193
1194                                 oldself = self;
1195                                 self = self.enemy;
1196                                 activator = self.pusher;
1197                                 SUB_UseTargets();
1198                                 self = oldself;
1199                         }
1200                 }
1201         }
1202
1203         if(self.dmg)
1204         {
1205                 if(self.team)
1206                         if(((self.spawnflags & 8) == 0) == (self.team != hitent.team))
1207                                 return;
1208                 if(hitent.takedamage)
1209                         Damage(hitent, self, self, ((self.dmg < 0) ? 100000 : (self.dmg * frametime)), DEATH_HURTTRIGGER, hitloc, '0 0 0');
1210         }
1211 }
1212
1213 float laser_SendEntity(entity to, float fl)
1214 {
1215         WriteByte(MSG_ENTITY, ENT_CLIENT_LASER);
1216         fl = fl - (fl & 0xF0); // use that bit to indicate finite length laser
1217         if(self.spawnflags & 2)
1218                 fl |= 0x80;
1219         if(self.alpha)
1220                 fl |= 0x40;
1221         if(self.scale != 1 || self.modelscale != 1)
1222                 fl |= 0x20;
1223         if(self.spawnflags & 4)
1224                 fl |= 0x10;
1225         WriteByte(MSG_ENTITY, fl);
1226         if(fl & 1)
1227         {
1228                 WriteCoord(MSG_ENTITY, self.origin_x);
1229                 WriteCoord(MSG_ENTITY, self.origin_y);
1230                 WriteCoord(MSG_ENTITY, self.origin_z);
1231         }
1232         if(fl & 8)
1233         {
1234                 WriteByte(MSG_ENTITY, self.colormod_x * 255.0);
1235                 WriteByte(MSG_ENTITY, self.colormod_y * 255.0);
1236                 WriteByte(MSG_ENTITY, self.colormod_z * 255.0);
1237                 if(fl & 0x40)
1238                         WriteByte(MSG_ENTITY, self.alpha * 255.0);
1239                 if(fl & 0x20)
1240                 {
1241                         WriteByte(MSG_ENTITY, bound(0, self.scale * 16.0, 255));
1242                         WriteByte(MSG_ENTITY, bound(0, self.modelscale * 16.0, 255));
1243                 }
1244                 if((fl & 0x80) || !(fl & 0x10)) // effect doesn't need sending if the laser is infinite and has collision testing turned off
1245                         WriteShort(MSG_ENTITY, self.cnt + 1);
1246         }
1247         if(fl & 2)
1248         {
1249                 if(fl & 0x80)
1250                 {
1251                         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
1252                         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
1253                         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
1254                 }
1255                 else
1256                 {
1257                         WriteAngle(MSG_ENTITY, self.mangle_x);
1258                         WriteAngle(MSG_ENTITY, self.mangle_y);
1259                 }
1260         }
1261         if(fl & 4)
1262                 WriteByte(MSG_ENTITY, self.state);
1263         return 1;
1264 }
1265
1266 /*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
1267 Any object touching the beam will be hurt
1268 Keys:
1269 "target"
1270  spawnfunc_target_position where the laser ends
1271 "mdl"
1272  name of beam end effect to use
1273 "colormod"
1274  color of the beam (default: red)
1275 "dmg"
1276  damage per second (-1 for a laser that kills immediately)
1277 */
1278 void laser_use()
1279 {
1280         self.state = !self.state;
1281         self.SendFlags |= 4;
1282         misc_laser_aim();
1283 }
1284
1285 void laser_reset()
1286 {
1287         if(self.spawnflags & 1)
1288                 self.state = 1;
1289         else
1290                 self.state = 0;
1291 }
1292
1293 void spawnfunc_misc_laser()
1294 {
1295         if(self.mdl)
1296         {
1297                 if(self.mdl == "none")
1298                         self.cnt = -1;
1299                 else
1300                 {
1301                         self.cnt = particleeffectnum(self.mdl);
1302                         if(self.cnt < 0)
1303                                 if(self.dmg)
1304                                         self.cnt = particleeffectnum("laser_deadly");
1305                 }
1306         }
1307         else if(!self.cnt)
1308         {
1309                 if(self.dmg)
1310                         self.cnt = particleeffectnum("laser_deadly");
1311                 else
1312                         self.cnt = -1;
1313         }
1314         if(self.cnt < 0)
1315                 self.cnt = -1;
1316
1317         if(self.colormod == '0 0 0')
1318                 if(!self.alpha)
1319                         self.colormod = '1 0 0';
1320         if(self.message == "")
1321                 self.message = "saw the light";
1322         if (self.message2 == "")
1323                 self.message2 = "was pushed into a laser by";
1324         if(!self.scale)
1325                 self.scale = 1;
1326         if(!self.modelscale)
1327                 self.modelscale = 1;
1328         else if(self.modelscale < 0)
1329                 self.modelscale = 0;
1330         self.think = misc_laser_think;
1331         self.nextthink = time;
1332         InitializeEntity(self, misc_laser_init, INITPRIO_FINDTARGET);
1333
1334         self.mangle = self.angles;
1335
1336         Net_LinkEntity(self, FALSE, 0, laser_SendEntity);
1337
1338         IFTARGETED
1339         {
1340                 self.reset = laser_reset;
1341                 laser_reset();
1342                 self.use = laser_use;
1343         }
1344         else
1345                 self.state = 1;
1346 }
1347
1348 // tZorks trigger impulse / gravity
1349 .float radius;
1350 .float falloff;
1351 .float strength;
1352 .float lastpushtime;
1353
1354 // targeted (directional) mode
1355 void trigger_impulse_touch1()
1356 {
1357         entity targ;
1358     float pushdeltatime;
1359     float str;
1360
1361         if (self.active != ACTIVE_ACTIVE)
1362                 return;
1363
1364         if (!isPushable(other))
1365                 return;
1366
1367         EXACTTRIGGER_TOUCH;
1368
1369     targ = find(world, targetname, self.target);
1370     if(!targ)
1371     {
1372         objerror("trigger_force without a (valid) .target!\n");
1373         remove(self);
1374         return;
1375     }
1376
1377     str = min(self.radius, vlen(self.origin - other.origin));
1378
1379     if(self.falloff == 1)
1380         str = (str / self.radius) * self.strength;
1381     else if(self.falloff == 2)
1382         str = (1 - (str / self.radius)) * self.strength;
1383     else
1384         str = self.strength;
1385
1386     pushdeltatime = time - other.lastpushtime;
1387     if (pushdeltatime > 0.15) pushdeltatime = 0;
1388     other.lastpushtime = time;
1389     if(!pushdeltatime) return;
1390
1391     other.velocity = other.velocity + normalize(targ.origin - self.origin) * str * pushdeltatime;
1392     other.flags &= ~FL_ONGROUND;
1393     UpdateCSQCProjectile(other);
1394 }
1395
1396 // Directionless (accelerator/decelerator) mode
1397 void trigger_impulse_touch2()
1398 {
1399     float pushdeltatime;
1400
1401         if (self.active != ACTIVE_ACTIVE)
1402                 return;
1403
1404         if (!isPushable(other))
1405                 return;
1406
1407         EXACTTRIGGER_TOUCH;
1408
1409     pushdeltatime = time - other.lastpushtime;
1410     if (pushdeltatime > 0.15) pushdeltatime = 0;
1411     other.lastpushtime = time;
1412     if(!pushdeltatime) return;
1413
1414     // div0: ticrate independent, 1 = identity (not 20)
1415     other.velocity = other.velocity * pow(self.strength, pushdeltatime);
1416     UpdateCSQCProjectile(other);
1417 }
1418
1419 // Spherical (gravity/repulsor) mode
1420 void trigger_impulse_touch3()
1421 {
1422     float pushdeltatime;
1423     float str;
1424
1425         if (self.active != ACTIVE_ACTIVE)
1426                 return;
1427
1428         if (!isPushable(other))
1429                 return;
1430
1431         EXACTTRIGGER_TOUCH;
1432
1433     pushdeltatime = time - other.lastpushtime;
1434     if (pushdeltatime > 0.15) pushdeltatime = 0;
1435     other.lastpushtime = time;
1436     if(!pushdeltatime) return;
1437
1438     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1439
1440         str = min(self.radius, vlen(self.origin - other.origin));
1441
1442     if(self.falloff == 1)
1443         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1444     else if(self.falloff == 2)
1445         str = (str / self.radius) * self.strength; // 0 in the inside
1446     else
1447         str = self.strength;
1448
1449     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1450     UpdateCSQCProjectile(other);
1451 }
1452
1453 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1454 -------- KEYS --------
1455 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1456          If not, this trigger acts like a damper/accelerator field.
1457
1458 strength : This is how mutch force to add in the direction of .target each second
1459            when .target is set. If not, this is hoe mutch to slow down/accelerate
1460            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1461
1462 radius   : If set, act as a spherical device rather then a liniar one.
1463
1464 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1465
1466 -------- NOTES --------
1467 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1468 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1469 */
1470
1471 void spawnfunc_trigger_impulse()
1472 {
1473         self.active = ACTIVE_ACTIVE;
1474
1475         EXACTTRIGGER_INIT;
1476     if(self.radius)
1477     {
1478         if(!self.strength) self.strength = 2000 * autocvar_g_triggerimpulse_radial_multiplier;
1479         setorigin(self, self.origin);
1480         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1481         self.touch = trigger_impulse_touch3;
1482     }
1483     else
1484     {
1485         if(self.target)
1486         {
1487             if(!self.strength) self.strength = 950 * autocvar_g_triggerimpulse_directional_multiplier;
1488             self.touch = trigger_impulse_touch1;
1489         }
1490         else
1491         {
1492             if(!self.strength) self.strength = 0.9;
1493                         self.strength = pow(self.strength, autocvar_g_triggerimpulse_accel_power) * autocvar_g_triggerimpulse_accel_multiplier;
1494             self.touch = trigger_impulse_touch2;
1495         }
1496     }
1497 }
1498
1499 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1500 "Flip-flop" trigger gate... lets only every second trigger event through
1501 */
1502 void flipflop_use()
1503 {
1504         self.state = !self.state;
1505         if(self.state)
1506                 SUB_UseTargets();
1507 }
1508
1509 void spawnfunc_trigger_flipflop()
1510 {
1511         if(self.spawnflags & 1)
1512                 self.state = 1;
1513         self.use = flipflop_use;
1514         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1515 }
1516
1517 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1518 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1519 */
1520 void monoflop_use()
1521 {
1522         self.nextthink = time + self.wait;
1523         self.enemy = activator;
1524         if(self.state)
1525                 return;
1526         self.state = 1;
1527         SUB_UseTargets();
1528 }
1529 void monoflop_fixed_use()
1530 {
1531         if(self.state)
1532                 return;
1533         self.nextthink = time + self.wait;
1534         self.state = 1;
1535         self.enemy = activator;
1536         SUB_UseTargets();
1537 }
1538
1539 void monoflop_think()
1540 {
1541         self.state = 0;
1542         activator = self.enemy;
1543         SUB_UseTargets();
1544 }
1545
1546 void monoflop_reset()
1547 {
1548         self.state = 0;
1549         self.nextthink = 0;
1550 }
1551
1552 void spawnfunc_trigger_monoflop()
1553 {
1554         if(!self.wait)
1555                 self.wait = 1;
1556         if(self.spawnflags & 1)
1557                 self.use = monoflop_fixed_use;
1558         else
1559                 self.use = monoflop_use;
1560         self.think = monoflop_think;
1561         self.state = 0;
1562         self.reset = monoflop_reset;
1563 }
1564
1565 void multivibrator_send()
1566 {
1567         float newstate;
1568         float cyclestart;
1569
1570         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1571
1572         newstate = (time < cyclestart + self.wait);
1573
1574         activator = self;
1575         if(self.state != newstate)
1576                 SUB_UseTargets();
1577         self.state = newstate;
1578
1579         if(self.state)
1580                 self.nextthink = cyclestart + self.wait + 0.01;
1581         else
1582                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1583 }
1584
1585 void multivibrator_toggle()
1586 {
1587         if(self.nextthink == 0)
1588         {
1589                 multivibrator_send();
1590         }
1591         else
1592         {
1593                 if(self.state)
1594                 {
1595                         SUB_UseTargets();
1596                         self.state = 0;
1597                 }
1598                 self.nextthink = 0;
1599         }
1600 }
1601
1602 void multivibrator_reset()
1603 {
1604         if(!(self.spawnflags & 1))
1605                 self.nextthink = 0; // wait for a trigger event
1606         else
1607                 self.nextthink = max(1, time);
1608 }
1609
1610 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1611 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1612 -------- KEYS --------
1613 target: trigger all entities with this targetname when it goes off
1614 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1615 phase: offset of the timing
1616 wait: "on" cycle time (default: 1)
1617 respawntime: "off" cycle time (default: same as wait)
1618 -------- SPAWNFLAGS --------
1619 START_ON: assume it is already turned on (when targeted)
1620 */
1621 void spawnfunc_trigger_multivibrator()
1622 {
1623         if(!self.wait)
1624                 self.wait = 1;
1625         if(!self.respawntime)
1626                 self.respawntime = self.wait;
1627
1628         self.state = 0;
1629         self.use = multivibrator_toggle;
1630         self.think = multivibrator_send;
1631         self.nextthink = max(1, time);
1632
1633         IFTARGETED
1634                 multivibrator_reset();
1635 }
1636
1637
1638 void follow_init()
1639 {
1640         entity src, dst;
1641         src = world;
1642         dst = world;
1643         if(self.killtarget != "")
1644                 src = find(world, targetname, self.killtarget);
1645         if(self.target != "")
1646                 dst = find(world, targetname, self.target);
1647
1648         if(!src && !dst)
1649         {
1650                 objerror("follow: could not find target/killtarget");
1651                 return;
1652         }
1653
1654         if(self.jointtype)
1655         {
1656                 // already done :P entity must stay
1657                 self.aiment = src;
1658                 self.enemy = dst;
1659         }
1660         else if(!src || !dst)
1661         {
1662                 objerror("follow: could not find target/killtarget");
1663                 return;
1664         }
1665         else if(self.spawnflags & 1)
1666         {
1667                 // attach
1668                 if(self.spawnflags & 2)
1669                 {
1670                         setattachment(dst, src, self.message);
1671                 }
1672                 else
1673                 {
1674                         attach_sameorigin(dst, src, self.message);
1675                 }
1676
1677                 dst.solid = SOLID_NOT; // solid doesn't work with attachment
1678                 remove(self);
1679         }
1680         else
1681         {
1682                 if(self.spawnflags & 2)
1683                 {
1684                         dst.movetype = MOVETYPE_FOLLOW;
1685                         dst.aiment = src;
1686                         // dst.punchangle = '0 0 0'; // keep unchanged
1687                         dst.view_ofs = dst.origin;
1688                         dst.v_angle = dst.angles;
1689                 }
1690                 else
1691                 {
1692                         follow_sameorigin(dst, src);
1693                 }
1694
1695                 remove(self);
1696         }
1697 }
1698
1699 void spawnfunc_misc_follow()
1700 {
1701         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1702 }
1703
1704
1705
1706 void gamestart_use() {
1707         activator = self;
1708         SUB_UseTargets();
1709         remove(self);
1710 }
1711
1712 void spawnfunc_trigger_gamestart() {
1713         self.use = gamestart_use;
1714         self.reset2 = spawnfunc_trigger_gamestart;
1715
1716         if(self.wait)
1717         {
1718                 self.think = self.use;
1719                 self.nextthink = game_starttime + self.wait;
1720         }
1721         else
1722                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1723 }
1724
1725
1726
1727
1728 .entity voicescript; // attached voice script
1729 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1730 .float voicescript_nextthink; // time to play next voice
1731 .float voicescript_voiceend; // time when this voice ends
1732
1733 void target_voicescript_clear(entity pl)
1734 {
1735         pl.voicescript = world;
1736 }
1737
1738 void target_voicescript_use()
1739 {
1740         if(activator.voicescript != self)
1741         {
1742                 activator.voicescript = self;
1743                 activator.voicescript_index = 0;
1744                 activator.voicescript_nextthink = time + self.delay;
1745         }
1746 }
1747
1748 void target_voicescript_next(entity pl)
1749 {
1750         entity vs;
1751         float i, n, dt;
1752
1753         vs = pl.voicescript;
1754         if(!vs)
1755                 return;
1756         if(vs.message == "")
1757                 return;
1758         if (!IS_PLAYER(pl))
1759                 return;
1760         if(gameover)
1761                 return;
1762
1763         if(time >= pl.voicescript_voiceend)
1764         {
1765                 if(time >= pl.voicescript_nextthink)
1766                 {
1767                         // get the next voice...
1768                         n = tokenize_console(vs.message);
1769
1770                         if(pl.voicescript_index < vs.cnt)
1771                                 i = pl.voicescript_index * 2;
1772                         else if(n > vs.cnt * 2)
1773                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1774                         else
1775                                 i = -1;
1776
1777                         if(i >= 0)
1778                         {
1779                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1780                                 dt = stof(argv(i + 1));
1781                                 if(dt >= 0)
1782                                 {
1783                                         pl.voicescript_voiceend = time + dt;
1784                                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1785                                 }
1786                                 else
1787                                 {
1788                                         pl.voicescript_voiceend = time - dt;
1789                                         pl.voicescript_nextthink = pl.voicescript_voiceend;
1790                                 }
1791
1792                                 pl.voicescript_index += 1;
1793                         }
1794                         else
1795                         {
1796                                 pl.voicescript = world; // stop trying then
1797                         }
1798                 }
1799         }
1800 }
1801
1802 void spawnfunc_target_voicescript()
1803 {
1804         // netname: directory of the sound files
1805         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1806         //          foo1 4.1 foo2 4.0 foo3 -3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1807         //          Here, a - in front of the duration means that no delay is to be
1808         //          added after this message
1809         // wait: average time between messages
1810         // delay: initial delay before the first message
1811
1812         float i, n;
1813         self.use = target_voicescript_use;
1814
1815         n = tokenize_console(self.message);
1816         self.cnt = n / 2;
1817         for(i = 0; i+1 < n; i += 2)
1818         {
1819                 if(argv(i) == "*")
1820                 {
1821                         self.cnt = i / 2;
1822                         ++i;
1823                 }
1824                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1825         }
1826 }
1827
1828
1829
1830 void trigger_relay_teamcheck_use()
1831 {
1832         if(activator.team)
1833         {
1834                 if(self.spawnflags & 2)
1835                 {
1836                         if(activator.team != self.team)
1837                                 SUB_UseTargets();
1838                 }
1839                 else
1840                 {
1841                         if(activator.team == self.team)
1842                                 SUB_UseTargets();
1843                 }
1844         }
1845         else
1846         {
1847                 if(self.spawnflags & 1)
1848                         SUB_UseTargets();
1849         }
1850 }
1851
1852 void trigger_relay_teamcheck_reset()
1853 {
1854         self.team = self.team_saved;
1855 }
1856
1857 void spawnfunc_trigger_relay_teamcheck()
1858 {
1859         self.team_saved = self.team;
1860         self.use = trigger_relay_teamcheck_use;
1861         self.reset = trigger_relay_teamcheck_reset;
1862 }
1863
1864
1865
1866 void trigger_disablerelay_use()
1867 {
1868         entity e;
1869
1870         float a, b;
1871         a = b = 0;
1872
1873         for(e = world; (e = find(e, targetname, self.target)); )
1874         {
1875                 if(e.use == SUB_UseTargets)
1876                 {
1877                         e.use = SUB_DontUseTargets;
1878                         ++a;
1879                 }
1880                 else if(e.use == SUB_DontUseTargets)
1881                 {
1882                         e.use = SUB_UseTargets;
1883                         ++b;
1884                 }
1885         }
1886
1887         if((!a) == (!b))
1888                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1889 }
1890
1891 void spawnfunc_trigger_disablerelay()
1892 {
1893         self.use = trigger_disablerelay_use;
1894 }
1895
1896 float magicear_matched;
1897 float W_Tuba_HasPlayed(entity pl, string melody, float instrument, float ignorepitch, float mintempo, float maxtempo);
1898 string trigger_magicear_processmessage(entity ear, entity source, float teamsay, entity privatesay, string msgin)
1899 {
1900         float domatch, dotrigger, matchstart, l;
1901         string s, msg;
1902         entity oldself;
1903         string savemessage;
1904
1905         magicear_matched = FALSE;
1906
1907         dotrigger = ((IS_PLAYER(source)) && (source.deadflag == DEAD_NO) && ((ear.radius == 0) || (vlen(source.origin - ear.origin) <= ear.radius)));
1908         domatch = ((ear.spawnflags & 32) || dotrigger);
1909
1910         if (!domatch)
1911                 return msgin;
1912
1913         if (!msgin)
1914         {
1915                 // we are in TUBA mode!
1916                 if (!(ear.spawnflags & 256))
1917                         return msgin;
1918
1919                 if(!W_Tuba_HasPlayed(source, ear.message, ear.movedir_x, !(ear.spawnflags & 512), ear.movedir_y, ear.movedir_z))
1920                         return msgin;
1921
1922                 magicear_matched = TRUE;
1923
1924                 if(dotrigger)
1925                 {
1926                         oldself = self;
1927                         activator = source;
1928                         self = ear;
1929                         savemessage = self.message;
1930                         self.message = string_null;
1931                         SUB_UseTargets();
1932                         self.message = savemessage;
1933                         self = oldself;
1934                 }
1935
1936                 if(ear.netname != "")
1937                         return ear.netname;
1938
1939                 return msgin;
1940         }
1941
1942         if(ear.spawnflags & 256) // ENOTUBA
1943                 return msgin;
1944
1945         if(privatesay)
1946         {
1947                 if(ear.spawnflags & 4)
1948                         return msgin;
1949         }
1950         else
1951         {
1952                 if(!teamsay)
1953                         if(ear.spawnflags & 1)
1954                                 return msgin;
1955                 if(teamsay > 0)
1956                         if(ear.spawnflags & 2)
1957                                 return msgin;
1958                 if(teamsay < 0)
1959                         if(ear.spawnflags & 8)
1960                                 return msgin;
1961         }
1962
1963         matchstart = -1;
1964         l = strlen(ear.message);
1965
1966         if(ear.spawnflags & 128)
1967                 msg = msgin;
1968         else
1969                 msg = strdecolorize(msgin);
1970
1971         if(substring(ear.message, 0, 1) == "*")
1972         {
1973                 if(substring(ear.message, -1, 1) == "*")
1974                 {
1975                         // two wildcards
1976                         // as we need multi-replacement here...
1977                         s = substring(ear.message, 1, -2);
1978                         l -= 2;
1979                         if(strstrofs(msg, s, 0) >= 0)
1980                                 matchstart = -2; // we use strreplace on s
1981                 }
1982                 else
1983                 {
1984                         // match at start
1985                         s = substring(ear.message, 1, -1);
1986                         l -= 1;
1987                         if(substring(msg, -l, l) == s)
1988                                 matchstart = strlen(msg) - l;
1989                 }
1990         }
1991         else
1992         {
1993                 if(substring(ear.message, -1, 1) == "*")
1994                 {
1995                         // match at end
1996                         s = substring(ear.message, 0, -2);
1997                         l -= 1;
1998                         if(substring(msg, 0, l) == s)
1999                                 matchstart = 0;
2000                 }
2001                 else
2002                 {
2003                         // full match
2004                         s = ear.message;
2005                         if(msg == ear.message)
2006                                 matchstart = 0;
2007                 }
2008         }
2009
2010         if(matchstart == -1) // no match
2011                 return msgin;
2012
2013         magicear_matched = TRUE;
2014
2015         if(dotrigger)
2016         {
2017                 oldself = self;
2018                 activator = source;
2019                 self = ear;
2020                 savemessage = self.message;
2021                 self.message = string_null;
2022                 SUB_UseTargets();
2023                 self.message = savemessage;
2024                 self = oldself;
2025         }
2026
2027         if(ear.spawnflags & 16)
2028         {
2029                 return ear.netname;
2030         }
2031         else if(ear.netname != "")
2032         {
2033                 if(matchstart < 0)
2034                         return strreplace(s, ear.netname, msg);
2035                 else
2036                         return strcat(
2037                                 substring(msg, 0, matchstart),
2038                                 ear.netname,
2039                                 substring(msg, matchstart + l, -1)
2040                         );
2041         }
2042         else
2043                 return msgin;
2044 }
2045
2046 entity magicears;
2047 string trigger_magicear_processmessage_forallears(entity source, float teamsay, entity privatesay, string msgin)
2048 {
2049         entity ear;
2050         string msgout;
2051         for(ear = magicears; ear; ear = ear.enemy)
2052         {
2053                 msgout = trigger_magicear_processmessage(ear, source, teamsay, privatesay, msgin);
2054                 if (!(ear.spawnflags & 64))
2055                         if(magicear_matched)
2056                                 return msgout;
2057                 msgin = msgout;
2058         }
2059         return msgin;
2060 }
2061
2062 void spawnfunc_trigger_magicear()
2063 {
2064         self.enemy = magicears;
2065         magicears = self;
2066
2067         // actually handled in "say" processing
2068         // spawnflags:
2069         //    1 = ignore say
2070         //    2 = ignore teamsay
2071         //    4 = ignore tell
2072         //    8 = ignore tell to unknown player
2073         //   16 = let netname replace the whole message (otherwise, netname is a word replacement if set)
2074         //   32 = perform the replacement even if outside the radius or dead
2075         //   64 = continue replacing/triggering even if this one matched
2076         //  128 = don't decolorize message before matching
2077         //  256 = message is a tuba note sequence (pitch.duration pitch.duration ...)
2078         //  512 = tuba notes must be exact right pitch, no transposing
2079         // message: either
2080         //   *pattern*
2081         // or
2082         //   *pattern
2083         // or
2084         //   pattern*
2085         // or
2086         //   pattern
2087         // netname:
2088         //   if set, replacement for the matched text
2089         // radius:
2090         //   "hearing distance"
2091         // target:
2092         //   what to trigger
2093         // movedir:
2094         //   for spawnflags 256, defines 'instrument+1 mintempo maxtempo' (zero component doesn't matter)
2095
2096         self.movedir_x -= 1; // map to tuba instrument numbers
2097 }
2098
2099 void relay_activators_use()
2100 {
2101         entity trg, os;
2102
2103         os = self;
2104
2105         for(trg = world; (trg = find(trg, targetname, os.target)); )
2106         {
2107                 self = trg;
2108                 if (trg.setactive)
2109                         trg.setactive(os.cnt);
2110                 else
2111                 {
2112                         //bprint("Not using setactive\n");
2113                         if(os.cnt == ACTIVE_TOGGLE)
2114                                 if(trg.active == ACTIVE_ACTIVE)
2115                                         trg.active = ACTIVE_NOT;
2116                                 else
2117                                         trg.active = ACTIVE_ACTIVE;
2118                         else
2119                                 trg.active = os.cnt;
2120                 }
2121         }
2122         self = os;
2123 }
2124
2125 void spawnfunc_relay_activate()
2126 {
2127         self.cnt = ACTIVE_ACTIVE;
2128         self.use = relay_activators_use;
2129 }
2130
2131 void spawnfunc_relay_deactivate()
2132 {
2133         self.cnt = ACTIVE_NOT;
2134         self.use = relay_activators_use;
2135 }
2136
2137 void spawnfunc_relay_activatetoggle()
2138 {
2139         self.cnt = ACTIVE_TOGGLE;
2140         self.use = relay_activators_use;
2141 }
2142
2143 .string chmap, gametype;
2144 void spawnfunc_target_changelevel_use()
2145 {
2146         if(self.gametype != "")
2147                 MapInfo_SwitchGameType(MapInfo_Type_FromString(self.gametype));
2148
2149         if (self.chmap == "")
2150                 localcmd("endmatch\n");
2151         else
2152                 localcmd(strcat("changelevel ", self.chmap, "\n"));
2153 }
2154
2155 void spawnfunc_target_changelevel()
2156 {
2157         self.use = spawnfunc_target_changelevel_use;
2158 }