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