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