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