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