]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_triggers.qc
Revert admin message & trigger messages
[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 not(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 not(self.spawnflags & 2)
207                 if not(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 not(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 not(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 not(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
1093 void FireRailgunBullet (vector start, vector end, float bdamage, float bforce, float mindist, float maxdist, float halflifedist, float forcehalflifedist, float deathtype);
1094
1095 .float modelscale;
1096 void misc_laser_aim()
1097 {
1098         vector a;
1099         if(self.enemy)
1100         {
1101                 if(self.spawnflags & 2)
1102                 {
1103                         if(self.enemy.origin != self.mangle)
1104                         {
1105                                 self.mangle = self.enemy.origin;
1106                                 self.SendFlags |= 2;
1107                         }
1108                 }
1109                 else
1110                 {
1111                         a = vectoangles(self.enemy.origin - self.origin);
1112                         a_x = -a_x;
1113                         if(a != self.mangle)
1114                         {
1115                                 self.mangle = a;
1116                                 self.SendFlags |= 2;
1117                         }
1118                 }
1119         }
1120         else
1121         {
1122                 if(self.angles != self.mangle)
1123                 {
1124                         self.mangle = self.angles;
1125                         self.SendFlags |= 2;
1126                 }
1127         }
1128         if(self.origin != self.oldorigin)
1129         {
1130                 self.SendFlags |= 1;
1131                 self.oldorigin = self.origin;
1132         }
1133 }
1134
1135 void misc_laser_init()
1136 {
1137         if(self.target != "")
1138                 self.enemy = find(world, targetname, self.target);
1139 }
1140
1141 .entity pusher;
1142 void misc_laser_think()
1143 {
1144         vector o;
1145         entity oldself;
1146         entity hitent;
1147         vector hitloc;
1148
1149         self.nextthink = time;
1150
1151         if(!self.state)
1152                 return;
1153
1154         misc_laser_aim();
1155
1156         if(self.enemy)
1157         {
1158                 o = self.enemy.origin;
1159                 if not(self.spawnflags & 2)
1160                         o = self.origin + normalize(o - self.origin) * 32768;
1161         }
1162         else
1163         {
1164                 makevectors(self.mangle);
1165                 o = self.origin + v_forward * 32768;
1166         }
1167
1168         if(self.dmg || self.enemy.target != "")
1169         {
1170                 traceline(self.origin, o, MOVE_NORMAL, self);
1171         }
1172         hitent = trace_ent;
1173         hitloc = trace_endpos;
1174
1175         if(self.enemy.target != "") // DETECTOR laser
1176         {
1177                 if(trace_ent.iscreature)
1178                 {
1179                         self.pusher = hitent;
1180                         if(!self.count)
1181                         {
1182                                 self.count = 1;
1183
1184                                 oldself = self;
1185                                 self = self.enemy;
1186                                 activator = self.pusher;
1187                                 SUB_UseTargets();
1188                                 self = oldself;
1189                         }
1190                 }
1191                 else
1192                 {
1193                         if(self.count)
1194                         {
1195                                 self.count = 0;
1196
1197                                 oldself = self;
1198                                 self = self.enemy;
1199                                 activator = self.pusher;
1200                                 SUB_UseTargets();
1201                                 self = oldself;
1202                         }
1203                 }
1204         }
1205
1206         if(self.dmg)
1207         {
1208                 if(self.team)
1209                         if(((self.spawnflags & 8) == 0) == (self.team != hitent.team))
1210                                 return;
1211                 if(hitent.takedamage)
1212                         Damage(hitent, self, self, ((self.dmg < 0) ? 100000 : (self.dmg * frametime)), DEATH_HURTTRIGGER, hitloc, '0 0 0');
1213         }
1214 }
1215
1216 float laser_SendEntity(entity to, float fl)
1217 {
1218         WriteByte(MSG_ENTITY, ENT_CLIENT_LASER);
1219         fl = fl - (fl & 0xF0); // use that bit to indicate finite length laser
1220         if(self.spawnflags & 2)
1221                 fl |= 0x80;
1222         if(self.alpha)
1223                 fl |= 0x40;
1224         if(self.scale != 1 || self.modelscale != 1)
1225                 fl |= 0x20;
1226         if(self.spawnflags & 4)
1227                 fl |= 0x10;
1228         WriteByte(MSG_ENTITY, fl);
1229         if(fl & 1)
1230         {
1231                 WriteCoord(MSG_ENTITY, self.origin_x);
1232                 WriteCoord(MSG_ENTITY, self.origin_y);
1233                 WriteCoord(MSG_ENTITY, self.origin_z);
1234         }
1235         if(fl & 8)
1236         {
1237                 WriteByte(MSG_ENTITY, self.colormod_x * 255.0);
1238                 WriteByte(MSG_ENTITY, self.colormod_y * 255.0);
1239                 WriteByte(MSG_ENTITY, self.colormod_z * 255.0);
1240                 if(fl & 0x40)
1241                         WriteByte(MSG_ENTITY, self.alpha * 255.0);
1242                 if(fl & 0x20)
1243                 {
1244                         WriteByte(MSG_ENTITY, bound(0, self.scale * 16.0, 255));
1245                         WriteByte(MSG_ENTITY, bound(0, self.modelscale * 16.0, 255));
1246                 }
1247                 if((fl & 0x80) || !(fl & 0x10)) // effect doesn't need sending if the laser is infinite and has collision testing turned off
1248                         WriteShort(MSG_ENTITY, self.cnt + 1);
1249         }
1250         if(fl & 2)
1251         {
1252                 if(fl & 0x80)
1253                 {
1254                         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
1255                         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
1256                         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
1257                 }
1258                 else
1259                 {
1260                         WriteAngle(MSG_ENTITY, self.mangle_x);
1261                         WriteAngle(MSG_ENTITY, self.mangle_y);
1262                 }
1263         }
1264         if(fl & 4)
1265                 WriteByte(MSG_ENTITY, self.state);
1266         return 1;
1267 }
1268
1269 /*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
1270 Any object touching the beam will be hurt
1271 Keys:
1272 "target"
1273  spawnfunc_target_position where the laser ends
1274 "mdl"
1275  name of beam end effect to use
1276 "colormod"
1277  color of the beam (default: red)
1278 "dmg"
1279  damage per second (-1 for a laser that kills immediately)
1280 */
1281 void laser_use()
1282 {
1283         self.state = !self.state;
1284         self.SendFlags |= 4;
1285         misc_laser_aim();
1286 }
1287
1288 void laser_reset()
1289 {
1290         if(self.spawnflags & 1)
1291                 self.state = 1;
1292         else
1293                 self.state = 0;
1294 }
1295
1296 void spawnfunc_misc_laser()
1297 {
1298         if(self.mdl)
1299         {
1300                 if(self.mdl == "none")
1301                         self.cnt = -1;
1302                 else
1303                 {
1304                         self.cnt = particleeffectnum(self.mdl);
1305                         if(self.cnt < 0)
1306                                 if(self.dmg)
1307                                         self.cnt = particleeffectnum("laser_deadly");
1308                 }
1309         }
1310         else if(!self.cnt)
1311         {
1312                 if(self.dmg)
1313                         self.cnt = particleeffectnum("laser_deadly");
1314                 else
1315                         self.cnt = -1;
1316         }
1317         if(self.cnt < 0)
1318                 self.cnt = -1;
1319
1320         if(self.colormod == '0 0 0')
1321                 if(!self.alpha)
1322                         self.colormod = '1 0 0';
1323         if(self.message == "")
1324                 self.message = "saw the light";
1325         if (self.message2 == "")
1326                 self.message2 = "was pushed into a laser by";
1327         if(!self.scale)
1328                 self.scale = 1;
1329         if(!self.modelscale)
1330                 self.modelscale = 1;
1331         else if(self.modelscale < 0)
1332                 self.modelscale = 0;
1333         self.think = misc_laser_think;
1334         self.nextthink = time;
1335         InitializeEntity(self, misc_laser_init, INITPRIO_FINDTARGET);
1336
1337         self.mangle = self.angles;
1338
1339         Net_LinkEntity(self, FALSE, 0, laser_SendEntity);
1340
1341         IFTARGETED
1342         {
1343                 self.reset = laser_reset;
1344                 laser_reset();
1345                 self.use = laser_use;
1346         }
1347         else
1348                 self.state = 1;
1349 }
1350
1351 // tZorks trigger impulse / gravity
1352 .float radius;
1353 .float falloff;
1354 .float strength;
1355 .float lastpushtime;
1356
1357 // targeted (directional) mode
1358 void trigger_impulse_touch1()
1359 {
1360         entity targ;
1361     float pushdeltatime;
1362     float str;
1363
1364         if (self.active != ACTIVE_ACTIVE) 
1365                 return;
1366
1367         if (!isPushable(other))
1368                 return;
1369
1370         EXACTTRIGGER_TOUCH;
1371
1372     targ = find(world, targetname, self.target);
1373     if(!targ)
1374     {
1375         objerror("trigger_force without a (valid) .target!\n");
1376         remove(self);
1377         return;
1378     }
1379
1380     str = min(self.radius, vlen(self.origin - other.origin));
1381
1382     if(self.falloff == 1)
1383         str = (str / self.radius) * self.strength;
1384     else if(self.falloff == 2)
1385         str = (1 - (str / self.radius)) * self.strength;
1386     else
1387         str = self.strength;
1388
1389     pushdeltatime = time - other.lastpushtime;
1390     if (pushdeltatime > 0.15) pushdeltatime = 0;
1391     other.lastpushtime = time;
1392     if(!pushdeltatime) return;
1393
1394     other.velocity = other.velocity + normalize(targ.origin - self.origin) * str * pushdeltatime;
1395     other.flags &= ~FL_ONGROUND;
1396     UpdateCSQCProjectile(other);
1397 }
1398
1399 // Directionless (accelerator/decelerator) mode
1400 void trigger_impulse_touch2()
1401 {
1402     float pushdeltatime;
1403
1404         if (self.active != ACTIVE_ACTIVE) 
1405                 return;
1406
1407         if (!isPushable(other))
1408                 return;
1409
1410         EXACTTRIGGER_TOUCH;
1411
1412     pushdeltatime = time - other.lastpushtime;
1413     if (pushdeltatime > 0.15) pushdeltatime = 0;
1414     other.lastpushtime = time;
1415     if(!pushdeltatime) return;
1416
1417     // div0: ticrate independent, 1 = identity (not 20)
1418     other.velocity = other.velocity * pow(self.strength, pushdeltatime);
1419     UpdateCSQCProjectile(other);
1420 }
1421
1422 // Spherical (gravity/repulsor) mode
1423 void trigger_impulse_touch3()
1424 {
1425     float pushdeltatime;
1426     float str;
1427
1428         if (self.active != ACTIVE_ACTIVE) 
1429                 return;
1430
1431         if (!isPushable(other))
1432                 return;
1433
1434         EXACTTRIGGER_TOUCH;
1435
1436     pushdeltatime = time - other.lastpushtime;
1437     if (pushdeltatime > 0.15) pushdeltatime = 0;
1438     other.lastpushtime = time;
1439     if(!pushdeltatime) return;
1440
1441     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1442
1443         str = min(self.radius, vlen(self.origin - other.origin));
1444
1445     if(self.falloff == 1)
1446         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1447     else if(self.falloff == 2)
1448         str = (str / self.radius) * self.strength; // 0 in the inside
1449     else
1450         str = self.strength;
1451
1452     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1453     UpdateCSQCProjectile(other);
1454 }
1455
1456 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1457 -------- KEYS --------
1458 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1459          If not, this trigger acts like a damper/accelerator field.
1460
1461 strength : This is how mutch force to add in the direction of .target each second
1462            when .target is set. If not, this is hoe mutch to slow down/accelerate
1463            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1464
1465 radius   : If set, act as a spherical device rather then a liniar one.
1466
1467 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1468
1469 -------- NOTES --------
1470 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1471 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1472 */
1473
1474 void spawnfunc_trigger_impulse()
1475 {
1476         self.active = ACTIVE_ACTIVE;
1477
1478         EXACTTRIGGER_INIT;
1479     if(self.radius)
1480     {
1481         if(!self.strength) self.strength = 2000 * autocvar_g_triggerimpulse_radial_multiplier;
1482         setorigin(self, self.origin);
1483         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1484         self.touch = trigger_impulse_touch3;
1485     }
1486     else
1487     {
1488         if(self.target)
1489         {
1490             if(!self.strength) self.strength = 950 * autocvar_g_triggerimpulse_directional_multiplier;
1491             self.touch = trigger_impulse_touch1;
1492         }
1493         else
1494         {
1495             if(!self.strength) self.strength = 0.9;
1496                         self.strength = pow(self.strength, autocvar_g_triggerimpulse_accel_power) * autocvar_g_triggerimpulse_accel_multiplier;
1497             self.touch = trigger_impulse_touch2;
1498         }
1499     }
1500 }
1501
1502 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1503 "Flip-flop" trigger gate... lets only every second trigger event through
1504 */
1505 void flipflop_use()
1506 {
1507         self.state = !self.state;
1508         if(self.state)
1509                 SUB_UseTargets();
1510 }
1511
1512 void spawnfunc_trigger_flipflop()
1513 {
1514         if(self.spawnflags & 1)
1515                 self.state = 1;
1516         self.use = flipflop_use;
1517         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1518 }
1519
1520 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1521 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1522 */
1523 void monoflop_use()
1524 {
1525         self.nextthink = time + self.wait;
1526         self.enemy = activator;
1527         if(self.state)
1528                 return;
1529         self.state = 1;
1530         SUB_UseTargets();
1531 }
1532 void monoflop_fixed_use()
1533 {
1534         if(self.state)
1535                 return;
1536         self.nextthink = time + self.wait;
1537         self.state = 1;
1538         self.enemy = activator;
1539         SUB_UseTargets();
1540 }
1541
1542 void monoflop_think()
1543 {
1544         self.state = 0;
1545         activator = self.enemy;
1546         SUB_UseTargets();
1547 }
1548
1549 void monoflop_reset()
1550 {
1551         self.state = 0;
1552         self.nextthink = 0;
1553 }
1554
1555 void spawnfunc_trigger_monoflop()
1556 {
1557         if(!self.wait)
1558                 self.wait = 1;
1559         if(self.spawnflags & 1)
1560                 self.use = monoflop_fixed_use;
1561         else
1562                 self.use = monoflop_use;
1563         self.think = monoflop_think;
1564         self.state = 0;
1565         self.reset = monoflop_reset;
1566 }
1567
1568 void multivibrator_send()
1569 {
1570         float newstate;
1571         float cyclestart;
1572
1573         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1574
1575         newstate = (time < cyclestart + self.wait);
1576
1577         activator = self;
1578         if(self.state != newstate)
1579                 SUB_UseTargets();
1580         self.state = newstate;
1581
1582         if(self.state)
1583                 self.nextthink = cyclestart + self.wait + 0.01;
1584         else
1585                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1586 }
1587
1588 void multivibrator_toggle()
1589 {
1590         if(self.nextthink == 0)
1591         {
1592                 multivibrator_send();
1593         }
1594         else
1595         {
1596                 if(self.state)
1597                 {
1598                         SUB_UseTargets();
1599                         self.state = 0;
1600                 }
1601                 self.nextthink = 0;
1602         }
1603 }
1604
1605 void multivibrator_reset()
1606 {
1607         if(!(self.spawnflags & 1))
1608                 self.nextthink = 0; // wait for a trigger event
1609         else
1610                 self.nextthink = max(1, time);
1611 }
1612
1613 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1614 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1615 -------- KEYS --------
1616 target: trigger all entities with this targetname when it goes off
1617 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1618 phase: offset of the timing
1619 wait: "on" cycle time (default: 1)
1620 respawntime: "off" cycle time (default: same as wait)
1621 -------- SPAWNFLAGS --------
1622 START_ON: assume it is already turned on (when targeted)
1623 */
1624 void spawnfunc_trigger_multivibrator()
1625 {
1626         if(!self.wait)
1627                 self.wait = 1;
1628         if(!self.respawntime)
1629                 self.respawntime = self.wait;
1630
1631         self.state = 0;
1632         self.use = multivibrator_toggle;
1633         self.think = multivibrator_send;
1634         self.nextthink = max(1, time);
1635
1636         IFTARGETED
1637                 multivibrator_reset();
1638 }
1639
1640
1641 void follow_init()
1642 {
1643         entity src, dst;
1644         src = world;
1645         dst = world;
1646         if(self.killtarget != "")
1647                 src = find(world, targetname, self.killtarget);
1648         if(self.target != "")
1649                 dst = find(world, targetname, self.target);
1650
1651         if(!src && !dst)
1652         {
1653                 objerror("follow: could not find target/killtarget");
1654                 return;
1655         }
1656
1657         if(self.jointtype)
1658         {
1659                 // already done :P entity must stay
1660                 self.aiment = src;
1661                 self.enemy = dst;
1662         }
1663         else if(!src || !dst)
1664         {
1665                 objerror("follow: could not find target/killtarget");
1666                 return;
1667         }
1668         else if(self.spawnflags & 1)
1669         {
1670                 // attach
1671                 if(self.spawnflags & 2)
1672                 {
1673                         setattachment(dst, src, self.message);
1674                 }
1675                 else
1676                 {
1677                         attach_sameorigin(dst, src, self.message);
1678                 }
1679
1680                 dst.solid = SOLID_NOT; // solid doesn't work with attachment
1681                 remove(self);
1682         }
1683         else
1684         {
1685                 if(self.spawnflags & 2)
1686                 {
1687                         dst.movetype = MOVETYPE_FOLLOW;
1688                         dst.aiment = src;
1689                         // dst.punchangle = '0 0 0'; // keep unchanged
1690                         dst.view_ofs = dst.origin;
1691                         dst.v_angle = dst.angles;
1692                 }
1693                 else
1694                 {
1695                         follow_sameorigin(dst, src);
1696                 }
1697
1698                 remove(self);
1699         }
1700 }
1701
1702 void spawnfunc_misc_follow()
1703 {
1704         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1705 }
1706
1707
1708
1709 void gamestart_use() {
1710         activator = self;
1711         SUB_UseTargets();
1712         remove(self);
1713 }
1714
1715 void spawnfunc_trigger_gamestart() {
1716         self.use = gamestart_use;
1717         self.reset2 = spawnfunc_trigger_gamestart;
1718
1719         if(self.wait)
1720         {
1721                 self.think = self.use;
1722                 self.nextthink = game_starttime + self.wait;
1723         }
1724         else
1725                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1726 }
1727
1728
1729
1730
1731 .entity voicescript; // attached voice script
1732 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1733 .float voicescript_nextthink; // time to play next voice
1734 .float voicescript_voiceend; // time when this voice ends
1735
1736 void target_voicescript_clear(entity pl)
1737 {
1738         pl.voicescript = world;
1739 }
1740
1741 void target_voicescript_use()
1742 {
1743         if(activator.voicescript != self)
1744         {
1745                 activator.voicescript = self;
1746                 activator.voicescript_index = 0;
1747                 activator.voicescript_nextthink = time + self.delay;
1748         }
1749 }
1750
1751 void target_voicescript_next(entity pl)
1752 {
1753         entity vs;
1754         float i, n, dt;
1755
1756         vs = pl.voicescript;
1757         if(!vs)
1758                 return;
1759         if(vs.message == "")
1760                 return;
1761         if not(IS_PLAYER(pl))
1762                 return;
1763         if(gameover)
1764                 return;
1765
1766         if(time >= pl.voicescript_voiceend)
1767         {
1768                 if(time >= pl.voicescript_nextthink)
1769                 {
1770                         // get the next voice...
1771                         n = tokenize_console(vs.message);
1772
1773                         if(pl.voicescript_index < vs.cnt)
1774                                 i = pl.voicescript_index * 2;
1775                         else if(n > vs.cnt * 2)
1776                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1777                         else
1778                                 i = -1;
1779
1780                         if(i >= 0)
1781                         {
1782                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1783                                 dt = stof(argv(i + 1));
1784                                 if(dt >= 0)
1785                                 {
1786                                         pl.voicescript_voiceend = time + dt;
1787                                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1788                                 }
1789                                 else
1790                                 {
1791                                         pl.voicescript_voiceend = time - dt;
1792                                         pl.voicescript_nextthink = pl.voicescript_voiceend;
1793                                 }
1794
1795                                 pl.voicescript_index += 1;
1796                         }
1797                         else
1798                         {
1799                                 pl.voicescript = world; // stop trying then
1800                         }
1801                 }
1802         }
1803 }
1804
1805 void spawnfunc_target_voicescript()
1806 {
1807         // netname: directory of the sound files
1808         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1809         //          foo1 4.1 foo2 4.0 foo3 -3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1810         //          Here, a - in front of the duration means that no delay is to be
1811         //          added after this message
1812         // wait: average time between messages
1813         // delay: initial delay before the first message
1814         
1815         float i, n;
1816         self.use = target_voicescript_use;
1817
1818         n = tokenize_console(self.message);
1819         self.cnt = n / 2;
1820         for(i = 0; i+1 < n; i += 2)
1821         {
1822                 if(argv(i) == "*")
1823                 {
1824                         self.cnt = i / 2;
1825                         ++i;
1826                 }
1827                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1828         }
1829 }
1830
1831
1832
1833 void trigger_relay_teamcheck_use()
1834 {
1835         if(activator.team)
1836         {
1837                 if(self.spawnflags & 2)
1838                 {
1839                         if(activator.team != self.team)
1840                                 SUB_UseTargets();
1841                 }
1842                 else
1843                 {
1844                         if(activator.team == self.team)
1845                                 SUB_UseTargets();
1846                 }
1847         }
1848         else
1849         {
1850                 if(self.spawnflags & 1)
1851                         SUB_UseTargets();
1852         }
1853 }
1854
1855 void trigger_relay_teamcheck_reset()
1856 {
1857         self.team = self.team_saved;
1858 }
1859
1860 void spawnfunc_trigger_relay_teamcheck()
1861 {
1862         self.team_saved = self.team;
1863         self.use = trigger_relay_teamcheck_use;
1864         self.reset = trigger_relay_teamcheck_reset;
1865 }
1866
1867
1868
1869 void trigger_disablerelay_use()
1870 {
1871         entity e;
1872
1873         float a, b;
1874         a = b = 0;
1875
1876         for(e = world; (e = find(e, targetname, self.target)); )
1877         {
1878                 if(e.use == SUB_UseTargets)
1879                 {
1880                         e.use = SUB_DontUseTargets;
1881                         ++a;
1882                 }
1883                 else if(e.use == SUB_DontUseTargets)
1884                 {
1885                         e.use = SUB_UseTargets;
1886                         ++b;
1887                 }
1888         }
1889
1890         if((!a) == (!b))
1891                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1892 }
1893
1894 void spawnfunc_trigger_disablerelay()
1895 {
1896         self.use = trigger_disablerelay_use;
1897 }
1898
1899 float magicear_matched;
1900 float W_Tuba_HasPlayed(entity pl, string melody, float instrument, float ignorepitch, float mintempo, float maxtempo);
1901 string trigger_magicear_processmessage(entity ear, entity source, float teamsay, entity privatesay, string msgin)
1902 {
1903         float domatch, dotrigger, matchstart, l;
1904         string s, msg;
1905         entity oldself;
1906         string savemessage;
1907
1908         magicear_matched = FALSE;
1909
1910         dotrigger = ((IS_PLAYER(source)) && (source.deadflag == DEAD_NO) && ((ear.radius == 0) || (vlen(source.origin - ear.origin) <= ear.radius)));
1911         domatch = ((ear.spawnflags & 32) || dotrigger);
1912
1913         if not(domatch)
1914                 return msgin;
1915
1916         if not(msgin)
1917         {
1918                 // we are in TUBA mode!
1919                 if not(ear.spawnflags & 256)
1920                         return msgin;
1921
1922                 if(!W_Tuba_HasPlayed(source, ear.message, ear.movedir_x, !(ear.spawnflags & 512), ear.movedir_y, ear.movedir_z))
1923                         return msgin;
1924
1925                 magicear_matched = TRUE;
1926
1927                 if(dotrigger)
1928                 {
1929                         oldself = self;
1930                         activator = source;
1931                         self = ear;
1932                         savemessage = self.message;
1933                         self.message = string_null;
1934                         SUB_UseTargets();
1935                         self.message = savemessage;
1936                         self = oldself;
1937                 }
1938
1939                 if(ear.netname != "")
1940                         return ear.netname;
1941
1942                 return msgin;
1943         }
1944
1945         if(ear.spawnflags & 256) // ENOTUBA
1946                 return msgin;
1947
1948         if(privatesay)
1949         {
1950                 if(ear.spawnflags & 4)
1951                         return msgin;
1952         }
1953         else
1954         {
1955                 if(!teamsay)
1956                         if(ear.spawnflags & 1)
1957                                 return msgin;
1958                 if(teamsay > 0)
1959                         if(ear.spawnflags & 2)
1960                                 return msgin;
1961                 if(teamsay < 0)
1962                         if(ear.spawnflags & 8)
1963                                 return msgin;
1964         }
1965         
1966         matchstart = -1;
1967         l = strlen(ear.message);
1968
1969         if(ear.spawnflags & 128)
1970                 msg = msgin;
1971         else
1972                 msg = strdecolorize(msgin);
1973
1974         if(substring(ear.message, 0, 1) == "*")
1975         {
1976                 if(substring(ear.message, -1, 1) == "*")
1977                 {
1978                         // two wildcards
1979                         // as we need multi-replacement here...
1980                         s = substring(ear.message, 1, -2);
1981                         l -= 2;
1982                         if(strstrofs(msg, s, 0) >= 0)
1983                                 matchstart = -2; // we use strreplace on s
1984                 }
1985                 else
1986                 {
1987                         // match at start
1988                         s = substring(ear.message, 1, -1);
1989                         l -= 1;
1990                         if(substring(msg, -l, l) == s)
1991                                 matchstart = strlen(msg) - l;
1992                 }
1993         }
1994         else
1995         {
1996                 if(substring(ear.message, -1, 1) == "*")
1997                 {
1998                         // match at end
1999                         s = substring(ear.message, 0, -2);
2000                         l -= 1;
2001                         if(substring(msg, 0, l) == s)
2002                                 matchstart = 0;
2003                 }
2004                 else
2005                 {
2006                         // full match
2007                         s = ear.message;
2008                         if(msg == ear.message)
2009                                 matchstart = 0;
2010                 }
2011         }
2012
2013         if(matchstart == -1) // no match
2014                 return msgin;
2015
2016         magicear_matched = TRUE;
2017
2018         if(dotrigger)
2019         {
2020                 oldself = self;
2021                 activator = source;
2022                 self = ear;
2023                 savemessage = self.message;
2024                 self.message = string_null;
2025                 SUB_UseTargets();
2026                 self.message = savemessage;
2027                 self = oldself;
2028         }
2029
2030         if(ear.spawnflags & 16)
2031         {
2032                 return ear.netname;
2033         }
2034         else if(ear.netname != "")
2035         {
2036                 if(matchstart < 0)
2037                         return strreplace(s, ear.netname, msg);
2038                 else
2039                         return strcat(
2040                                 substring(msg, 0, matchstart),
2041                                 ear.netname,
2042                                 substring(msg, matchstart + l, -1)
2043                         );
2044         }
2045         else
2046                 return msgin;
2047 }
2048
2049 entity magicears;
2050 string trigger_magicear_processmessage_forallears(entity source, float teamsay, entity privatesay, string msgin)
2051 {
2052         entity ear;
2053         string msgout;
2054         for(ear = magicears; ear; ear = ear.enemy)
2055         {
2056                 msgout = trigger_magicear_processmessage(ear, source, teamsay, privatesay, msgin);
2057                 if not(ear.spawnflags & 64)
2058                         if(magicear_matched)
2059                                 return msgout;
2060                 msgin = msgout;
2061         }
2062         return msgin;
2063 }
2064
2065 void spawnfunc_trigger_magicear()
2066 {
2067         self.enemy = magicears;
2068         magicears = self;
2069
2070         // actually handled in "say" processing
2071         // spawnflags:
2072         //    1 = ignore say
2073         //    2 = ignore teamsay
2074         //    4 = ignore tell
2075         //    8 = ignore tell to unknown player
2076         //   16 = let netname replace the whole message (otherwise, netname is a word replacement if set)
2077         //   32 = perform the replacement even if outside the radius or dead
2078         //   64 = continue replacing/triggering even if this one matched
2079         //  128 = don't decolorize message before matching
2080         //  256 = message is a tuba note sequence (pitch.duration pitch.duration ...)
2081         //  512 = tuba notes must be exact right pitch, no transposing
2082         // message: either
2083         //   *pattern*
2084         // or
2085         //   *pattern
2086         // or
2087         //   pattern*
2088         // or
2089         //   pattern
2090         // netname:
2091         //   if set, replacement for the matched text
2092         // radius:
2093         //   "hearing distance"
2094         // target:
2095         //   what to trigger
2096         // movedir:
2097         //   for spawnflags 256, defines 'instrument+1 mintempo maxtempo' (zero component doesn't matter)
2098
2099         self.movedir_x -= 1; // map to tuba instrument numbers
2100 }
2101
2102 void relay_activators_use()
2103 {
2104         entity trg, os;
2105         
2106         os = self;
2107         
2108         for(trg = world; (trg = find(trg, targetname, os.target)); )
2109         {
2110                 self = trg;
2111                 if (trg.setactive)
2112                         trg.setactive(os.cnt);
2113                 else
2114                 {
2115                         //bprint("Not using setactive\n");
2116                         if(os.cnt == ACTIVE_TOGGLE)
2117                                 if(trg.active == ACTIVE_ACTIVE)
2118                                         trg.active = ACTIVE_NOT;
2119                                 else    
2120                                         trg.active = ACTIVE_ACTIVE;
2121                         else
2122                                 trg.active = os.cnt;
2123                 }               
2124         }
2125         self = os;
2126 }
2127
2128 void spawnfunc_relay_activate()
2129 {
2130         self.cnt = ACTIVE_ACTIVE;
2131         self.use = relay_activators_use;
2132 }
2133
2134 void spawnfunc_relay_deactivate()
2135 {
2136         self.cnt = ACTIVE_NOT;
2137         self.use = relay_activators_use;        
2138 }
2139
2140 void spawnfunc_relay_activatetoggle()
2141 {
2142         self.cnt = ACTIVE_TOGGLE;
2143         self.use = relay_activators_use;        
2144 }
2145
2146 .string chmap, gametype;
2147 void spawnfunc_target_changelevel_use()
2148 {
2149         if(self.gametype != "")
2150                 MapInfo_SwitchGameType(MapInfo_Type_FromString(self.gametype));
2151
2152         if (self.chmap == "")
2153                 localcmd("endmatch\n");
2154         else
2155                 localcmd(strcat("changelevel ", self.chmap, "\n"));
2156 }
2157
2158 void spawnfunc_target_changelevel()
2159 {
2160         self.use = spawnfunc_target_changelevel_use;
2161 }