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