]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_triggers.qc
don't transform if the zone is faded out clearly (to reduce network entities)
[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         // self.message = "someone like %s always gets wrongplaced";
466
467         if(!trigger_hurt_first)
468                 trigger_hurt_first = self;
469         if(trigger_hurt_last)
470                 trigger_hurt_last.trigger_hurt_next = self;
471         trigger_hurt_last = self;
472 };
473
474 float tracebox_hits_trigger_hurt(vector start, vector mi, vector ma, vector end)
475 {
476         entity th;
477
478         for(th = trigger_hurt_first; th; th = th.trigger_hurt_next)
479                 if(tracebox_hits_box(start, mi, ma, end, th.absmin, th.absmax))
480                         return TRUE;
481
482         return FALSE;
483 }
484
485 //////////////////////////////////////////////////////////////
486 //
487 //
488 //
489 //Trigger heal --a04191b92fbd93aa67214ef7e72d6d2e
490 //
491 //////////////////////////////////////////////////////////////
492
493 .float triggerhealtime;
494 void trigger_heal_touch()
495 {
496         // only do the EXACTTRIGGER_TOUCH checks when really needed (saves some cpu)
497         if (other.iscreature)
498         {
499                 if (other.takedamage)
500                 if (other.triggerhealtime < time)
501                 {
502                         EXACTTRIGGER_TOUCH;
503                         other.triggerhealtime = time + 1;
504                         
505                         if (other.health < self.max_health)
506                         {
507                                 other.health = min(other.health + self.health, self.max_health);
508                                 other.pauserothealth_finished = max(other.pauserothealth_finished, time + cvar("g_balance_pause_health_rot"));
509                                 sound (other, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NORM);
510                         }
511                 }
512         }
513 };
514
515 void spawnfunc_trigger_heal()
516 {
517         EXACTTRIGGER_INIT;
518         self.touch = trigger_heal_touch;
519         if (!self.health)
520                 self.health = 10;
521         if (!self.max_health)
522                 self.max_health = 200; //Max health topoff for field
523         if(self.noise == "")
524                 self.noise = "misc/mediumhealth.wav";
525         precache_sound(self.noise);
526 };
527
528
529 //////////////////////////////////////////////////////////////
530 //
531 //
532 //
533 //End trigger_heal
534 //
535 //////////////////////////////////////////////////////////////
536
537 .entity trigger_gravity_check;
538 void trigger_gravity_remove(entity own)
539 {
540         if(own.trigger_gravity_check.owner == own)
541         {
542                 UpdateCSQCProjectile(own);
543                 own.gravity = own.trigger_gravity_check.gravity;
544                 remove(own.trigger_gravity_check);
545         }
546         else
547                 backtrace("Removing a trigger_gravity_check with no valid owner");
548         own.trigger_gravity_check = world;
549 }
550 void trigger_gravity_check_think()
551 {
552         // This spawns when a player enters the gravity zone and checks if he left.
553         // Each frame, self.count is set to 2 by trigger_gravity_touch() and decreased by 1 here.
554         // It the player has left the gravity trigger, this will be allowed to reach 0 and indicate that.
555         if(self.count <= 0)
556         {
557                 if(self.owner.trigger_gravity_check == self)
558                         trigger_gravity_remove(self.owner);
559                 else
560                         remove(self);
561                 return;
562         }
563         else
564         {
565                 self.count -= 1;
566                 self.nextthink = time;
567         }
568 };
569
570 void trigger_gravity_use()
571 {
572         self.state = !self.state;
573 };
574
575 void trigger_gravity_touch()
576 {
577         float g;
578
579         if(self.state != TRUE)
580                 return;
581
582         EXACTTRIGGER_TOUCH;
583
584         g = self.gravity;
585
586         if not(self.spawnflags & 1)
587         {
588                 if(other.trigger_gravity_check)
589                 {
590                         if(self == other.trigger_gravity_check.enemy)
591                         {
592                                 // same?
593                                 other.trigger_gravity_check.count = 2; // gravity one more frame...
594                                 return;
595                         }
596
597                         // compare prio
598                         if(self.cnt > other.trigger_gravity_check.enemy.cnt)
599                                 trigger_gravity_remove(other);
600                         else
601                                 return;
602                 }
603                 other.trigger_gravity_check = spawn();
604                 other.trigger_gravity_check.enemy = self;
605                 other.trigger_gravity_check.owner = other;
606                 other.trigger_gravity_check.gravity = other.gravity;
607                 other.trigger_gravity_check.think = trigger_gravity_check_think;
608                 other.trigger_gravity_check.nextthink = time;
609                 other.trigger_gravity_check.count = 2;
610                 if(other.gravity)
611                         g *= other.gravity;
612         }
613
614         if (other.gravity != g)
615         {
616                 other.gravity = g;
617                 if(self.noise != "")
618                         sound (other, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NORM);
619                 UpdateCSQCProjectile(self.owner);
620         }
621 };
622
623 void spawnfunc_trigger_gravity()
624 {
625         if(self.gravity == 1)
626                 return;
627
628         EXACTTRIGGER_INIT;
629         self.touch = trigger_gravity_touch;
630         if(self.noise != "")
631                 precache_sound(self.noise);
632
633         self.state = TRUE;
634         IFTARGETED
635         {
636                 self.use = trigger_gravity_use;
637                 if(self.spawnflags & 2)
638                         self.state = FALSE;
639         }
640 };
641
642 //=============================================================================
643
644 // TODO add a way to do looped sounds with sound(); then complete this entity
645 .float volume, atten;
646 void target_speaker_use() {sound(self, CHAN_TRIGGER, self.noise, VOL_BASE * self.volume, self.atten);}
647
648 void spawnfunc_target_speaker()
649 {
650         if(self.noise)
651                 precache_sound (self.noise);
652         IFTARGETED
653         {
654                 if(!self.atten)
655                         self.atten = ATTN_NORM;
656                 else if(self.atten < 0)
657                         self.atten = 0;
658                 if(!self.volume)
659                         self.volume = 1;
660                 self.use = target_speaker_use;
661         }
662         else
663         {
664                 if(!self.atten)
665                         self.atten = ATTN_STATIC;
666                 else if(self.atten < 0)
667                         self.atten = 0;
668                 if(!self.volume)
669                         self.volume = 1;
670                 ambientsound (self.origin, self.noise, VOL_BASE * self.volume, self.atten);
671         }
672 };
673
674
675 void spawnfunc_func_stardust() {
676         self.effects = EF_STARDUST;
677 }
678
679 .string bgmscript;
680 .float bgmscriptattack;
681 .float bgmscriptdecay;
682 .float bgmscriptsustain;
683 .float bgmscriptrelease;
684 float pointparticles_SendEntity(entity to, float fl)
685 {
686         WriteByte(MSG_ENTITY, ENT_CLIENT_POINTPARTICLES);
687
688         // optional features to save space
689         fl = fl & 0x0F;
690         if(self.spawnflags & 2)
691                 fl |= 0x10; // absolute count on toggle-on
692         if(self.movedir != '0 0 0' || self.velocity != '0 0 0')
693                 fl |= 0x20; // 4 bytes - saves CPU
694         if(self.waterlevel || self.count != 1)
695                 fl |= 0x40; // 4 bytes - obscure features almost never used
696         if(self.mins != '0 0 0' || self.maxs != '0 0 0')
697                 fl |= 0x80; // 14 bytes - saves lots of space
698
699         WriteByte(MSG_ENTITY, fl);
700         if(fl & 2)
701         {
702                 if(self.state)
703                         WriteCoord(MSG_ENTITY, self.impulse);
704                 else
705                         WriteCoord(MSG_ENTITY, 0); // off
706         }
707         if(fl & 4)
708         {
709                 WriteCoord(MSG_ENTITY, self.origin_x);
710                 WriteCoord(MSG_ENTITY, self.origin_y);
711                 WriteCoord(MSG_ENTITY, self.origin_z);
712         }
713         if(fl & 1)
714         {
715                 if(self.model != "null")
716                 {
717                         WriteShort(MSG_ENTITY, self.modelindex);
718                         if(fl & 0x80)
719                         {
720                                 WriteCoord(MSG_ENTITY, self.mins_x);
721                                 WriteCoord(MSG_ENTITY, self.mins_y);
722                                 WriteCoord(MSG_ENTITY, self.mins_z);
723                                 WriteCoord(MSG_ENTITY, self.maxs_x);
724                                 WriteCoord(MSG_ENTITY, self.maxs_y);
725                                 WriteCoord(MSG_ENTITY, self.maxs_z);
726                         }
727                 }
728                 else
729                 {
730                         WriteShort(MSG_ENTITY, 0);
731                         if(fl & 0x80)
732                         {
733                                 WriteCoord(MSG_ENTITY, self.maxs_x);
734                                 WriteCoord(MSG_ENTITY, self.maxs_y);
735                                 WriteCoord(MSG_ENTITY, self.maxs_z);
736                         }
737                 }
738                 WriteShort(MSG_ENTITY, self.cnt);
739                 if(fl & 0x20)
740                 {
741                         WriteShort(MSG_ENTITY, compressShortVector(self.velocity));
742                         WriteShort(MSG_ENTITY, compressShortVector(self.movedir));
743                 }
744                 if(fl & 0x40)
745                 {
746                         WriteShort(MSG_ENTITY, self.waterlevel * 16.0);
747                         WriteByte(MSG_ENTITY, self.count * 16.0);
748                 }
749                 WriteString(MSG_ENTITY, self.noise);
750                 if(self.noise != "")
751                 {
752                         WriteByte(MSG_ENTITY, floor(self.atten * 64));
753                         WriteByte(MSG_ENTITY, floor(self.volume * 255));
754                 }
755                 WriteString(MSG_ENTITY, self.bgmscript);
756                 if(self.bgmscript != "")
757                 {
758                         WriteByte(MSG_ENTITY, floor(self.bgmscriptattack * 64));
759                         WriteByte(MSG_ENTITY, floor(self.bgmscriptdecay * 64));
760                         WriteByte(MSG_ENTITY, floor(self.bgmscriptsustain * 255));
761                         WriteByte(MSG_ENTITY, floor(self.bgmscriptrelease * 64));
762                 }
763         }
764         return 1;
765 }
766
767 void pointparticles_use()
768 {
769         self.state = !self.state;
770         self.SendFlags |= 2;
771 }
772
773 void pointparticles_think()
774 {
775         if(self.origin != self.oldorigin)
776         {
777                 self.SendFlags |= 4;
778                 self.oldorigin = self.origin;
779         }
780         self.nextthink = time;
781 }
782
783 void pointparticles_reset()
784 {
785         if(self.spawnflags & 1)
786                 self.state = 1;
787         else
788                 self.state = 0;
789 }
790
791 void spawnfunc_func_pointparticles()
792 {
793         if(self.model != "")
794                 setmodel(self, self.model);
795         if(self.noise != "")
796                 precache_sound (self.noise);
797         
798         if(!self.bgmscriptsustain)
799                 self.bgmscriptsustain = 1;
800         else if(self.bgmscriptsustain < 0)
801                 self.bgmscriptsustain = 0;
802
803         if(!self.atten)
804                 self.atten = ATTN_NORM;
805         else if(self.atten < 0)
806                 self.atten = 0;
807         if(!self.volume)
808                 self.volume = 1;
809         if(!self.count)
810                 self.count = 1;
811         if(!self.impulse)
812                 self.impulse = 1;
813
814         if(!self.modelindex)
815         {
816                 setorigin(self, self.origin + self.mins);
817                 setsize(self, '0 0 0', self.maxs - self.mins);
818         }
819         if(!self.cnt)
820                 self.cnt = particleeffectnum(self.mdl);
821
822         Net_LinkEntity(self, (self.spawnflags & 4), 0, pointparticles_SendEntity);
823
824         IFTARGETED
825         {
826                 self.use = pointparticles_use;
827                 self.reset = pointparticles_reset;
828                 self.reset();
829         }
830         else
831                 self.state = 1;
832         self.think = pointparticles_think;
833         self.nextthink = time;
834 }
835
836 void spawnfunc_func_sparks()
837 {
838         // self.cnt is the amount of sparks that one burst will spawn
839         if(self.cnt < 1) {
840                 self.cnt = 25.0; // nice default value
841         }
842
843         // self.wait is the probability that a sparkthink will spawn a spark shower
844         // range: 0 - 1, but 0 makes little sense, so...
845         if(self.wait < 0.05) {
846                 self.wait = 0.25; // nice default value
847         }
848
849         self.count = self.cnt;
850         self.mins = '0 0 0';
851         self.maxs = '0 0 0';
852         self.velocity = '0 0 -1';
853         self.mdl = "TE_SPARK";
854         self.impulse = 10 * self.wait; // by default 2.5/sec
855         self.wait = 0;
856         self.cnt = 0; // use mdl
857
858         spawnfunc_func_pointparticles();
859 }
860
861 float rainsnow_SendEntity(entity to, float sf)
862 {
863         WriteByte(MSG_ENTITY, ENT_CLIENT_RAINSNOW);
864         WriteByte(MSG_ENTITY, self.state);
865         WriteCoord(MSG_ENTITY, self.origin_x + self.mins_x);
866         WriteCoord(MSG_ENTITY, self.origin_y + self.mins_y);
867         WriteCoord(MSG_ENTITY, self.origin_z + self.mins_z);
868         WriteCoord(MSG_ENTITY, self.maxs_x - self.mins_x);
869         WriteCoord(MSG_ENTITY, self.maxs_y - self.mins_y);
870         WriteCoord(MSG_ENTITY, self.maxs_z - self.mins_z);
871         WriteShort(MSG_ENTITY, compressShortVector(self.dest));
872         WriteShort(MSG_ENTITY, self.count);
873         WriteByte(MSG_ENTITY, self.cnt);
874         return 1;
875 };
876
877 /*QUAKED spawnfunc_func_rain (0 .5 .8) ?
878 This is an invisible area like a trigger, which rain falls inside of.
879
880 Keys:
881 "velocity"
882  falling direction (should be something like '0 0 -700', use the X and Y velocity for wind)
883 "cnt"
884  sets color of rain (default 12 - white)
885 "count"
886  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
887 */
888 void spawnfunc_func_rain()
889 {
890         self.dest = self.velocity;
891         self.velocity = '0 0 0';
892         if (!self.dest)
893                 self.dest = '0 0 -700';
894         self.angles = '0 0 0';
895         self.movetype = MOVETYPE_NONE;
896         self.solid = SOLID_NOT;
897         SetBrushEntityModel();
898         if (!self.cnt)
899                 self.cnt = 12;
900         if (!self.count)
901                 self.count = 2000;
902         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
903         if (self.count < 1)
904                 self.count = 1;
905         if(self.count > 65535)
906                 self.count = 65535;
907
908         self.state = 1; // 1 is rain, 0 is snow
909         self.Version = 1;
910
911         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
912 };
913
914
915 /*QUAKED spawnfunc_func_snow (0 .5 .8) ?
916 This is an invisible area like a trigger, which snow falls inside of.
917
918 Keys:
919 "velocity"
920  falling direction (should be something like '0 0 -300', use the X and Y velocity for wind)
921 "cnt"
922  sets color of rain (default 12 - white)
923 "count"
924  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
925 */
926 void spawnfunc_func_snow()
927 {
928         self.dest = self.velocity;
929         self.velocity = '0 0 0';
930         if (!self.dest)
931                 self.dest = '0 0 -300';
932         self.angles = '0 0 0';
933         self.movetype = MOVETYPE_NONE;
934         self.solid = SOLID_NOT;
935         SetBrushEntityModel();
936         if (!self.cnt)
937                 self.cnt = 12;
938         if (!self.count)
939                 self.count = 2000;
940         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
941         if (self.count < 1)
942                 self.count = 1;
943         if(self.count > 65535)
944                 self.count = 65535;
945
946         self.state = 0; // 1 is rain, 0 is snow
947         self.Version = 1;
948
949         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
950 };
951
952
953 void FireRailgunBullet (vector start, vector end, float bdamage, float bforce, float mindist, float maxdist, float halflifedist, float forcehalflifedist, float deathtype);
954
955 .float modelscale;
956 void misc_laser_aim()
957 {
958         vector a;
959         if(self.enemy)
960         {
961                 if(self.spawnflags & 2)
962                 {
963                         if(self.enemy.origin != self.mangle)
964                         {
965                                 self.mangle = self.enemy.origin;
966                                 self.SendFlags |= 2;
967                         }
968                 }
969                 else
970                 {
971                         a = vectoangles(self.enemy.origin - self.origin);
972                         a_x = -a_x;
973                         if(a != self.mangle)
974                         {
975                                 self.mangle = a;
976                                 self.SendFlags |= 2;
977                         }
978                 }
979         }
980         else
981         {
982                 if(self.angles != self.mangle)
983                 {
984                         self.mangle = self.angles;
985                         self.SendFlags |= 2;
986                 }
987         }
988         if(self.origin != self.oldorigin)
989         {
990                 self.SendFlags |= 1;
991                 self.oldorigin = self.origin;
992         }
993 }
994
995 void misc_laser_init()
996 {
997         if(self.target != "")
998                 self.enemy = find(world, targetname, self.target);
999 }
1000
1001 .entity pusher;
1002 void misc_laser_think()
1003 {
1004         vector o;
1005         entity oldself;
1006
1007         self.nextthink = time;
1008
1009         if(!self.state)
1010                 return;
1011
1012         misc_laser_aim();
1013
1014         if(self.enemy)
1015         {
1016                 o = self.enemy.origin;
1017                 if not(self.spawnflags & 2)
1018                         o = self.origin + normalize(o - self.origin) * 32768;
1019         }
1020         else
1021         {
1022                 makevectors(self.mangle);
1023                 o = self.origin + v_forward * 32768;
1024         }
1025
1026         if(self.dmg)
1027         {
1028                 if(self.dmg < 0)
1029                         FireRailgunBullet(self.origin, o, 100000, 0, 0, 0, 0, 0, DEATH_HURTTRIGGER);
1030                 else
1031                         FireRailgunBullet(self.origin, o, self.dmg * frametime, 0, 0, 0, 0, 0, DEATH_HURTTRIGGER);
1032         }
1033
1034         if(self.enemy.target != "") // DETECTOR laser
1035         {
1036                 traceline(self.origin, o, MOVE_NORMAL, self);
1037                 if(trace_ent.iscreature)
1038                 {
1039                         self.pusher = trace_ent;
1040                         if(!self.count)
1041                         {
1042                                 self.count = 1;
1043
1044                                 oldself = self;
1045                                 self = self.enemy;
1046                                 activator = self.pusher;
1047                                 SUB_UseTargets();
1048                                 self = oldself;
1049                         }
1050                 }
1051                 else
1052                 {
1053                         if(self.count)
1054                         {
1055                                 self.count = 0;
1056
1057                                 oldself = self;
1058                                 self = self.enemy;
1059                                 activator = self.pusher;
1060                                 SUB_UseTargets();
1061                                 self = oldself;
1062                         }
1063                 }
1064         }
1065 }
1066
1067 float laser_SendEntity(entity to, float fl)
1068 {
1069         WriteByte(MSG_ENTITY, ENT_CLIENT_LASER);
1070         fl = fl - (fl & 0xE0); // use that bit to indicate finite length laser
1071         if(self.spawnflags & 2)
1072                 fl |= 0x80;
1073         if(self.alpha)
1074                 fl |= 0x40;
1075         if(self.scale != 1 || self.modelscale != 1)
1076                 fl |= 0x20;
1077         WriteByte(MSG_ENTITY, fl);
1078         if(fl & 1)
1079         {
1080                 WriteCoord(MSG_ENTITY, self.origin_x);
1081                 WriteCoord(MSG_ENTITY, self.origin_y);
1082                 WriteCoord(MSG_ENTITY, self.origin_z);
1083         }
1084         if(fl & 8)
1085         {
1086                 WriteByte(MSG_ENTITY, self.colormod_x * 255.0);
1087                 WriteByte(MSG_ENTITY, self.colormod_y * 255.0);
1088                 WriteByte(MSG_ENTITY, self.colormod_z * 255.0);
1089                 if(fl & 0x40)
1090                         WriteByte(MSG_ENTITY, self.alpha * 255.0);
1091                 if(fl & 0x20)
1092                 {
1093                         WriteByte(MSG_ENTITY, bound(0, self.scale * 16.0, 255));
1094                         WriteByte(MSG_ENTITY, bound(0, self.modelscale * 16.0, 255));
1095                 }
1096                 WriteShort(MSG_ENTITY, self.cnt + 1);
1097         }
1098         if(fl & 2)
1099         {
1100                 if(fl & 0x80)
1101                 {
1102                         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
1103                         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
1104                         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
1105                 }
1106                 else
1107                 {
1108                         WriteAngle(MSG_ENTITY, self.mangle_x);
1109                         WriteAngle(MSG_ENTITY, self.mangle_y);
1110                 }
1111         }
1112         if(fl & 4)
1113                 WriteByte(MSG_ENTITY, self.state);
1114         return 1;
1115 }
1116
1117 /*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
1118 Any object touching the beam will be hurt
1119 Keys:
1120 "target"
1121  spawnfunc_target_position where the laser ends
1122 "mdl"
1123  name of beam end effect to use
1124 "colormod"
1125  color of the beam (default: red)
1126 "dmg"
1127  damage per second (-1 for a laser that kills immediately)
1128 */
1129 void laser_use()
1130 {
1131         self.state = !self.state;
1132         self.SendFlags |= 4;
1133         misc_laser_aim();
1134 }
1135
1136 void laser_reset()
1137 {
1138         if(self.spawnflags & 1)
1139                 self.state = 1;
1140         else
1141                 self.state = 0;
1142 }
1143
1144 void spawnfunc_misc_laser()
1145 {
1146         if(self.mdl)
1147         {
1148                 if(self.mdl == "none")
1149                         self.cnt = -1;
1150                 else
1151                 {
1152                         self.cnt = particleeffectnum(self.mdl);
1153                         if(self.cnt < 0)
1154                                 if(self.dmg)
1155                                         self.cnt = particleeffectnum("laser_deadly");
1156                 }
1157         }
1158         else if(!self.cnt)
1159         {
1160                 if(self.dmg)
1161                         self.cnt = particleeffectnum("laser_deadly");
1162                 else
1163                         self.cnt = -1;
1164         }
1165         if(self.cnt < 0)
1166                 self.cnt = -1;
1167
1168         if(self.colormod == '0 0 0')
1169                 if(!self.alpha)
1170                         self.colormod = '1 0 0';
1171         if(!self.message)
1172                 self.message = "saw the light";
1173         if (!self.message2)
1174                 self.message2 = "was pushed into a laser by";
1175         if(!self.scale)
1176                 self.scale = 1;
1177         if(!self.modelscale)
1178                 self.modelscale = 1;
1179         self.think = misc_laser_think;
1180         self.nextthink = time;
1181         InitializeEntity(self, misc_laser_init, INITPRIO_FINDTARGET);
1182
1183         self.mangle = self.angles;
1184
1185         Net_LinkEntity(self, FALSE, 0, laser_SendEntity);
1186
1187         IFTARGETED
1188         {
1189                 self.reset = laser_reset;
1190                 laser_reset();
1191                 self.use = laser_use;
1192         }
1193         else
1194                 self.state = 1;
1195 }
1196
1197 // tZorks trigger impulse / gravity
1198 .float radius;
1199 .float falloff;
1200 .float strength;
1201 .float lastpushtime;
1202
1203 // targeted (directional) mode
1204 void trigger_impulse_touch1()
1205 {
1206         entity targ;
1207     float pushdeltatime;
1208     float str;
1209
1210         // FIXME: Better checking for what to push and not.
1211         if not(other.iscreature)
1212         if (other.classname != "corpse")
1213         if (other.classname != "body")
1214         if (other.classname != "gib")
1215         if (other.classname != "missile")
1216         if (other.classname != "rocket")
1217         if (other.classname != "casing")
1218         if (other.classname != "grenade")
1219         if (other.classname != "plasma")
1220         if (other.classname != "plasma_prim")
1221         if (other.classname != "plasma_chain")
1222         if (other.classname != "droppedweapon")
1223         if (other.classname != "nexball_basketball")
1224         if (other.classname != "nexball_football")
1225                 return;
1226
1227         if (other.deadflag && other.iscreature)
1228                 return;
1229
1230         EXACTTRIGGER_TOUCH;
1231
1232     targ = find(world, targetname, self.target);
1233     if(!targ)
1234     {
1235         objerror("trigger_force without a (valid) .target!\n");
1236         remove(self);
1237         return;
1238     }
1239
1240     if(self.falloff == 1)
1241         str = (str / self.radius) * self.strength;
1242     else if(self.falloff == 2)
1243         str = (1 - (str / self.radius)) * self.strength;
1244     else
1245         str = self.strength;
1246
1247     pushdeltatime = time - other.lastpushtime;
1248     if (pushdeltatime > 0.15) pushdeltatime = 0;
1249     other.lastpushtime = time;
1250     if(!pushdeltatime) return;
1251
1252     other.velocity = other.velocity + normalize(targ.origin - self.origin) * str * pushdeltatime;
1253         other.flags &~= FL_ONGROUND;
1254 }
1255
1256 // Directionless (accelerator/decelerator) mode
1257 void trigger_impulse_touch2()
1258 {
1259     float pushdeltatime;
1260
1261         // FIXME: Better checking for what to push and not.
1262         if not(other.iscreature)
1263         if (other.classname != "corpse")
1264         if (other.classname != "body")
1265         if (other.classname != "gib")
1266         if (other.classname != "missile")
1267         if (other.classname != "rocket")
1268         if (other.classname != "casing")
1269         if (other.classname != "grenade")
1270         if (other.classname != "plasma")
1271         if (other.classname != "plasma_prim")
1272         if (other.classname != "plasma_chain")
1273         if (other.classname != "droppedweapon")
1274         if (other.classname != "nexball_basketball")
1275         if (other.classname != "nexball_football")
1276                 return;
1277
1278         if (other.deadflag && other.iscreature)
1279                 return;
1280
1281         EXACTTRIGGER_TOUCH;
1282
1283     pushdeltatime = time - other.lastpushtime;
1284     if (pushdeltatime > 0.15) pushdeltatime = 0;
1285     other.lastpushtime = time;
1286     if(!pushdeltatime) return;
1287
1288     // div0: ticrate independent, 1 = identity (not 20)
1289     other.velocity = other.velocity * pow(self.strength, pushdeltatime);
1290 }
1291
1292 // Spherical (gravity/repulsor) mode
1293 void trigger_impulse_touch3()
1294 {
1295     float pushdeltatime;
1296     float str;
1297
1298         // FIXME: Better checking for what to push and not.
1299         if not(other.iscreature)
1300         if (other.classname != "corpse")
1301         if (other.classname != "body")
1302         if (other.classname != "gib")
1303         if (other.classname != "missile")
1304         if (other.classname != "rocket")
1305         if (other.classname != "casing")
1306         if (other.classname != "grenade")
1307         if (other.classname != "plasma")
1308         if (other.classname != "plasma_prim")
1309         if (other.classname != "plasma_chain")
1310         if (other.classname != "droppedweapon")
1311         if (other.classname != "nexball_basketball")
1312         if (other.classname != "nexball_football")
1313                 return;
1314
1315         if (other.deadflag && other.iscreature)
1316                 return;
1317
1318         EXACTTRIGGER_TOUCH;
1319
1320     pushdeltatime = time - other.lastpushtime;
1321     if (pushdeltatime > 0.15) pushdeltatime = 0;
1322     other.lastpushtime = time;
1323     if(!pushdeltatime) return;
1324
1325     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1326
1327         str = min(self.radius, vlen(self.origin - other.origin));
1328
1329     if(self.falloff == 1)
1330         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1331     else if(self.falloff == 2)
1332         str = (str / self.radius) * self.strength; // 0 in the inside
1333     else
1334         str = self.strength;
1335
1336     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1337 }
1338
1339 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1340 -------- KEYS --------
1341 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1342          If not, this trigger acts like a damper/accelerator field.
1343
1344 strength : This is how mutch force to add in the direction of .target each second
1345            when .target is set. If not, this is hoe mutch to slow down/accelerate
1346            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1347
1348 radius   : If set, act as a spherical device rather then a liniar one.
1349
1350 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1351
1352 -------- NOTES --------
1353 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1354 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1355 */
1356
1357 void spawnfunc_trigger_impulse()
1358 {
1359         EXACTTRIGGER_INIT;
1360     if(self.radius)
1361     {
1362         if(!self.strength) self.strength = 2000 * cvar("g_triggerimpulse_radial_multiplier");
1363         setorigin(self, self.origin);
1364         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1365         self.touch = trigger_impulse_touch3;
1366     }
1367     else
1368     {
1369         if(self.target)
1370         {
1371             if(!self.strength) self.strength = 950 * cvar("g_triggerimpulse_directional_multiplier");
1372             self.touch = trigger_impulse_touch1;
1373         }
1374         else
1375         {
1376             if(!self.strength) self.strength = 0.9;
1377                         self.strength = pow(self.strength, cvar("g_triggerimpulse_accel_power")) * cvar("g_triggerimpulse_accel_multiplier");
1378             self.touch = trigger_impulse_touch2;
1379         }
1380     }
1381 }
1382
1383 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1384 "Flip-flop" trigger gate... lets only every second trigger event through
1385 */
1386 void flipflop_use()
1387 {
1388         self.state = !self.state;
1389         if(self.state)
1390                 SUB_UseTargets();
1391 }
1392
1393 void spawnfunc_trigger_flipflop()
1394 {
1395         if(self.spawnflags & 1)
1396                 self.state = 1;
1397         self.use = flipflop_use;
1398         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1399 }
1400
1401 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1402 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1403 */
1404 void monoflop_use()
1405 {
1406         self.nextthink = time + self.wait;
1407         self.enemy = activator;
1408         if(self.state)
1409                 return;
1410         self.state = 1;
1411         SUB_UseTargets();
1412 }
1413 void monoflop_fixed_use()
1414 {
1415         if(self.state)
1416                 return;
1417         self.nextthink = time + self.wait;
1418         self.state = 1;
1419         self.enemy = activator;
1420         SUB_UseTargets();
1421 }
1422
1423 void monoflop_think()
1424 {
1425         self.state = 0;
1426         activator = self.enemy;
1427         SUB_UseTargets();
1428 }
1429
1430 void monoflop_reset()
1431 {
1432         self.state = 0;
1433         self.nextthink = 0;
1434 }
1435
1436 void spawnfunc_trigger_monoflop()
1437 {
1438         if(!self.wait)
1439                 self.wait = 1;
1440         if(self.spawnflags & 1)
1441                 self.use = monoflop_fixed_use;
1442         else
1443                 self.use = monoflop_use;
1444         self.think = monoflop_think;
1445         self.state = 0;
1446         self.reset = monoflop_reset;
1447 }
1448
1449 void multivibrator_send()
1450 {
1451         float newstate;
1452         float cyclestart;
1453
1454         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1455
1456         newstate = (time < cyclestart + self.wait);
1457
1458         activator = self;
1459         if(self.state != newstate)
1460                 SUB_UseTargets();
1461         self.state = newstate;
1462
1463         if(self.state)
1464                 self.nextthink = cyclestart + self.wait + 0.01;
1465         else
1466                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1467 }
1468
1469 void multivibrator_toggle()
1470 {
1471         if(self.nextthink == 0)
1472         {
1473                 multivibrator_send();
1474         }
1475         else
1476         {
1477                 if(self.state)
1478                 {
1479                         SUB_UseTargets();
1480                         self.state = 0;
1481                 }
1482                 self.nextthink = 0;
1483         }
1484 }
1485
1486 void multivibrator_reset()
1487 {
1488         if(!(self.spawnflags & 1))
1489                 self.nextthink = 0; // wait for a trigger event
1490         else
1491                 self.nextthink = max(1, time);
1492 }
1493
1494 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1495 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1496 -------- KEYS --------
1497 target: trigger all entities with this targetname when it goes off
1498 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1499 phase: offset of the timing
1500 wait: "on" cycle time (default: 1)
1501 respawntime: "off" cycle time (default: same as wait)
1502 -------- SPAWNFLAGS --------
1503 START_ON: assume it is already turned on (when targeted)
1504 */
1505 void spawnfunc_trigger_multivibrator()
1506 {
1507         if(!self.wait)
1508                 self.wait = 1;
1509         if(!self.respawntime)
1510                 self.respawntime = self.wait;
1511
1512         self.state = 0;
1513         self.use = multivibrator_toggle;
1514         self.think = multivibrator_send;
1515         self.nextthink = time;
1516
1517         IFTARGETED
1518                 multivibrator_reset();
1519 }
1520
1521
1522 void follow_init()
1523 {
1524         entity src, dst;
1525         src = world;
1526         dst = world;
1527         if(self.killtarget != "")
1528                 src = find(world, targetname, self.killtarget);
1529         if(self.target != "")
1530                 dst = find(world, targetname, self.target);
1531
1532         if(!src && !dst)
1533         {
1534                 objerror("follow: could not find target/killtarget");
1535                 return;
1536         }
1537
1538         if(self.jointtype)
1539         {
1540                 // already done :P entity must stay
1541                 self.aiment = src;
1542                 self.enemy = dst;
1543         }
1544         else if(!src || !dst)
1545         {
1546                 objerror("follow: could not find target/killtarget");
1547                 return;
1548         }
1549         else if(self.spawnflags & 1)
1550         {
1551                 // attach
1552                 if(self.spawnflags & 2)
1553                 {
1554                         setattachment(dst, src, self.message);
1555                 }
1556                 else
1557                 {
1558                         attach_sameorigin(dst, src, self.message);
1559                 }
1560
1561                 remove(self);
1562         }
1563         else
1564         {
1565                 if(self.spawnflags & 2)
1566                 {
1567                         dst.movetype = MOVETYPE_FOLLOW;
1568                         dst.aiment = src;
1569                         // dst.punchangle = '0 0 0'; // keep unchanged
1570                         dst.view_ofs = dst.origin;
1571                         dst.v_angle = dst.angles;
1572                 }
1573                 else
1574                 {
1575                         follow_sameorigin(dst, src);
1576                 }
1577
1578                 remove(self);
1579         }
1580 }
1581
1582 void spawnfunc_misc_follow()
1583 {
1584         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1585 }
1586
1587
1588
1589 void gamestart_use() {
1590         activator = self;
1591         SUB_UseTargets();
1592         remove(self);
1593 }
1594
1595 void spawnfunc_trigger_gamestart() {
1596         self.use = gamestart_use;
1597         self.reset2 = spawnfunc_trigger_gamestart;
1598
1599         if(self.wait)
1600         {
1601                 self.think = self.use;
1602                 self.nextthink = game_starttime + self.wait;
1603         }
1604         else
1605                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1606 }
1607
1608
1609
1610
1611 .entity voicescript; // attached voice script
1612 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1613 .float voicescript_nextthink; // time to play next voice
1614 .float voicescript_voiceend; // time when this voice ends
1615
1616 void target_voicescript_clear(entity pl)
1617 {
1618         pl.voicescript = world;
1619 }
1620
1621 void target_voicescript_use()
1622 {
1623         if(activator.voicescript != self)
1624         {
1625                 activator.voicescript = self;
1626                 activator.voicescript_index = 0;
1627                 activator.voicescript_nextthink = time + self.delay;
1628         }
1629 }
1630
1631 void target_voicescript_next(entity pl)
1632 {
1633         entity vs;
1634         float i, n, dt;
1635
1636         vs = pl.voicescript;
1637         if(!vs)
1638                 return;
1639         if(vs.message == "")
1640                 return;
1641         if(pl.classname != "player")
1642                 return;
1643         if(gameover)
1644                 return;
1645
1646         if(time >= pl.voicescript_voiceend)
1647         {
1648                 if(time >= pl.voicescript_nextthink)
1649                 {
1650                         // get the next voice...
1651                         n = tokenize_console(vs.message);
1652
1653                         if(pl.voicescript_index < vs.cnt)
1654                                 i = pl.voicescript_index * 2;
1655                         else if(n > vs.cnt * 2)
1656                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1657                         else
1658                                 i = -1;
1659
1660                         if(i >= 0)
1661                         {
1662                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1663                                 dt = stof(argv(i + 1));
1664                                 if(dt >= 0)
1665                                 {
1666                                         pl.voicescript_voiceend = time + dt;
1667                                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1668                                 }
1669                                 else
1670                                 {
1671                                         pl.voicescript_voiceend = time - dt;
1672                                         pl.voicescript_nextthink = pl.voicescript_voiceend;
1673                                 }
1674
1675                                 pl.voicescript_index += 1;
1676                         }
1677                         else
1678                         {
1679                                 pl.voicescript = world; // stop trying then
1680                         }
1681                 }
1682         }
1683 }
1684
1685 void spawnfunc_target_voicescript()
1686 {
1687         // netname: directory of the sound files
1688         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1689         //          foo1 4.1 foo2 4.0 foo3 -3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1690         //          Here, a - in front of the duration means that no delay is to be
1691         //          added after this message
1692         // wait: average time between messages
1693         // delay: initial delay before the first message
1694         
1695         float i, n;
1696         self.use = target_voicescript_use;
1697
1698         n = tokenize_console(self.message);
1699         self.cnt = n / 2;
1700         for(i = 0; i+1 < n; i += 2)
1701         {
1702                 if(argv(i) == "*")
1703                 {
1704                         self.cnt = i / 2;
1705                         ++i;
1706                 }
1707                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1708         }
1709 }
1710
1711
1712
1713 void trigger_relay_teamcheck_use()
1714 {
1715         if(activator.team)
1716         {
1717                 if(self.spawnflags & 2)
1718                 {
1719                         if(activator.team != self.team)
1720                                 SUB_UseTargets();
1721                 }
1722                 else
1723                 {
1724                         if(activator.team == self.team)
1725                                 SUB_UseTargets();
1726                 }
1727         }
1728         else
1729         {
1730                 if(self.spawnflags & 1)
1731                         SUB_UseTargets();
1732         }
1733 }
1734
1735 void trigger_relay_teamcheck_reset()
1736 {
1737         self.team = self.team_saved;
1738 }
1739
1740 void spawnfunc_trigger_relay_teamcheck()
1741 {
1742         self.team_saved = self.team;
1743         self.use = trigger_relay_teamcheck_use;
1744         self.reset = trigger_relay_teamcheck_reset;
1745 }
1746
1747
1748
1749 void trigger_disablerelay_use()
1750 {
1751         entity e;
1752
1753         float a, b;
1754         a = b = 0;
1755
1756         for(e = world; (e = find(e, targetname, self.target)); )
1757         {
1758                 if(e.use == SUB_UseTargets)
1759                 {
1760                         e.use = SUB_DontUseTargets;
1761                         ++a;
1762                 }
1763                 else if(e.use == SUB_DontUseTargets)
1764                 {
1765                         e.use = SUB_UseTargets;
1766                         ++b;
1767                 }
1768         }
1769
1770         if((!a) == (!b))
1771                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1772 }
1773
1774 void spawnfunc_trigger_disablerelay()
1775 {
1776         self.use = trigger_disablerelay_use;
1777 }
1778
1779 float magicear_matched;
1780 string trigger_magicear_processmessage(entity ear, entity source, float teamsay, entity privatesay, string msgin)
1781 {
1782         float domatch, dotrigger, matchstart, l;
1783         string s, msg;
1784         entity oldself;
1785
1786         magicear_matched = FALSE;
1787
1788         dotrigger = ((self.classname == "player") && (self.deadflag == DEAD_NO) && ((ear.radius == 0) || (vlen(source.origin - ear.origin) <= ear.radius)));
1789         domatch = ((ear.spawnflags & 32) || dotrigger);
1790         if not(domatch)
1791                 return msgin;
1792
1793         if(privatesay)
1794         {
1795                 if(ear.spawnflags & 4)
1796                         return msgin;
1797         }
1798         else
1799         {
1800                 if(!teamsay)
1801                         if(ear.spawnflags & 1)
1802                                 return msgin;
1803                 if(teamsay > 0)
1804                         if(ear.spawnflags & 2)
1805                                 return msgin;
1806                 if(teamsay < 0)
1807                         if(ear.spawnflags & 8)
1808                                 return msgin;
1809         }
1810         
1811         matchstart = -1;
1812         l = strlen(ear.message);
1813
1814         if(self.spawnflags & 128)
1815                 msg = msgin;
1816         else
1817                 msg = strdecolorize(msgin);
1818
1819         if(substring(ear.message, 0, 1) == "*")
1820         {
1821                 if(substring(ear.message, -1, 1) == "*")
1822                 {
1823                         // two wildcards
1824                         // as we need multi-replacement here...
1825                         s = substring(ear.message, 1, -2);
1826                         l -= 2;
1827                         if(strstrofs(msg, s, 0) >= 0)
1828                                 matchstart = -2; // we use strreplace on s
1829                 }
1830                 else
1831                 {
1832                         // match at start
1833                         s = substring(ear.message, 1, -1);
1834                         l -= 1;
1835                         if(substring(msg, -l, l) == s)
1836                                 matchstart = strlen(msg) - l;
1837                 }
1838         }
1839         else
1840         {
1841                 if(substring(ear.message, -1, 1) == "*")
1842                 {
1843                         // match at end
1844                         s = substring(ear.message, 0, -2);
1845                         l -= 1;
1846                         if(substring(msg, 0, l) == s)
1847                                 matchstart = 0;
1848                 }
1849                 else
1850                 {
1851                         // full match
1852                         s = ear.message;
1853                         if(msg == ear.message)
1854                                 matchstart = 0;
1855                 }
1856         }
1857
1858         if(matchstart == -1) // no match
1859                 return msgin;
1860
1861         magicear_matched = TRUE;
1862
1863         if(dotrigger)
1864         {
1865                 oldself = activator = self;
1866                 self = ear;
1867                 SUB_UseTargets();
1868                 self = oldself;
1869         }
1870
1871         if(ear.spawnflags & 16)
1872         {
1873                 return ear.netname;
1874         }
1875         else if(ear.netname != "")
1876         {
1877                 if(matchstart < 0)
1878                         return strreplace(s, ear.netname, msg);
1879                 else
1880                         return strcat(
1881                                 substring(msg, 0, matchstart),
1882                                 ear.netname,
1883                                 substring(msg, matchstart + l, -1)
1884                         );
1885         }
1886         else
1887                 return msgin;
1888 }
1889
1890 entity magicears;
1891 string trigger_magicear_processmessage_forallears(entity source, float teamsay, entity privatesay, string msgin)
1892 {
1893         entity ear;
1894         string msgout;
1895         for(ear = magicears; ear; ear = ear.enemy)
1896         {
1897                 msgout = trigger_magicear_processmessage(ear, source, teamsay, privatesay, msgin);
1898                 if not(ear.spawnflags & 64)
1899                         if(magicear_matched)
1900                                 return msgout;
1901                 msgin = msgout;
1902         }
1903         return msgin;
1904 }
1905
1906 void spawnfunc_trigger_magicear()
1907 {
1908         self.enemy = magicears;
1909         magicears = self;
1910
1911         // actually handled in "say" processing
1912         // spawnflags:
1913         //   1 = ignore say
1914         //   2 = ignore teamsay
1915         //   4 = ignore tell
1916         //   8 = ignore tell to unknown player
1917         //   16 = let netname replace the whole message (otherwise, netname is a word replacement if set)
1918         //   32 = perform the replacement even if outside the radius or dead
1919         //   64 = continue replacing/triggering even if this one matched
1920         // message: either
1921         //   *pattern*
1922         // or
1923         //   *pattern
1924         // or
1925         //   pattern*
1926         // or
1927         //   pattern
1928         // netname:
1929         //   if set, replacement for the matched text
1930         // radius:
1931         //   "hearing distance"
1932         // target:
1933         //   what to trigger
1934 }