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